Zirak

@zirakertan

Exposing HomeObject

As I read more of the spec, I became more intrigued about [[HomeObject]]. [[HomeObject]] is used by the super keyword:

let parent = {
x: 'parent'
};
let child = {
x: 'child',
papaX() { return super.x; }
};
child.papaX(); // undefined
Object.setPrototypeOf(child, parent);
child.papaX(); // 'parent'

super when used inside of one of child's methods is basically the equivalent of Object.getPrototypeOf(child), whose value can change between call to call. However to do that a method (clarification: a method is a function like papaX, declared with the shorthand notation) needs to know which object it was defined in. As we all know, that can be wildly different from a function’s this value — this can be altered with call and friends alongside a myriad of other reasons.

Enter [[HomeObject]] . It’s a magical property assigned to methods at creation time which refers to the object they were created in. In the example above, super.x inside of papaX actually translates into:

Object.getPrototypeOf(papaX.[[HomeObject]]).x

Except that, of course, accessing the [[HomeObject]] of a function is outside the capabilities of regular JavaScript code.

That intrigued me. What if we did have the power to access the [[HomeObject]] of a function? If functions did have a constant binding to the object they were created in? I’m not saying it’s a good idea to have one, as a matter of fact I’m saying it’s a bad idea, but it is an interesting one.

So to ask the question: How do we implement accessing [[HomeObject]] in the various engines?

I’ll assume no prior knowledge on the JavaScript engines, how they’re implemented, C++, understanding of whales, breathing expertise, and most things in general. If you’ve ever wondered about engine internals I hope this will entertain and amaze!

V8

Let’s begin by asking where the hell V8’s source is. You might be tempted to answer that it’s on github. While that’s (partly) true, let me introduce you to the best website in the internet: https://cs.chromium.org

Let’s start out by optimistically searching for HomeObject

Looks promising! We can continue the search to see all search results, but let’s look at what we have in front of us:

  • kNeedsHomeObject doesn’t sound interesting to us right now, since it’ll probably only check if a function is a method.
  • HomeObjectSlot sounds better, it sounds like the place HomeObject is set, so maybe we can go there and look for call-ees?
  • Oh, TestHomeObject! Tests are always one of the best places for learning how a complex beast like v8 works, let’s open that:

Huh, that’s weird. What’s with the %HomeObjectSymbol()? What’s that % doing there, and why the heck are they calling HomeObject a symbol?

As it turns out, v8 is in part written in JavaScript (e.g. Array.prototype.reduce among many other functions!) which sometimes needs to use features which either can’t be written in plain js (e.g. run the GC, optimise this function, …), or which for performance reasons should be done in native code. Those special functions are denoted with a % prefix. We can actually use them with trusty ol’ node (which runs on v8) by passing a special --allow-natives-syntax flag:

I should probably go to sleep!

This is kind of cool! Hey, let’s get a list of all these functions. We happen to already have seen the name of one, HomeObjectSymbol, so let’s search for that:

There’s only ten results so nothing popping out as obvious isn’t too bad, we can go through one by one. And what do you know…the first result looks pretty promising:

Let’s see if they’re indeed all builtins or if they’re just there to tease us. Checking against two maybe-builtins from the above list:

The 2nd one was to test what happens if we try to run a non-existing builtin, and it looks like the other two are indeed builtins, so there we have it, the list of all builtins!

But wait wait wait, what led us down this rabbit hole? We were looking at how HomeObject was accessed, and found out about %HomeObjectSymbol . Let’s go back to our search to see where it’s implemented:

The macro call RUNTIME_FUNCTION inside of runtime-classes.cc looks promising, especially when the function was declared in runtime.h. Let’s take a looksie!

RUNTIME_FUNCTION(Runtime_HomeObjectSymbol) {
DCHECK_EQ(0, args.length());
return isolate->heap()->home_object_symbol();
}

Well that doesn’t tell us much, let’s click on home_object_access and see where it takes us:

#define SYMBOL_ACCESSOR(name) \
Symbol* Heap::name() { return Symbol::cast(roots_[k##name##RootIndex]); }
PRIVATE_SYMBOL_LIST(SYMBOL_ACCESSOR)
#undef SYMBOL_ACCESSOR

So I don’t know about you, but I’m not a C preprocessor so all of that looks like a bunch of gibberish to me. However, looking at that made me think: Zirak, you handsome creature, you spec of light in this dark universe, the reason I wake up in the morning, the pecan of my pie and gold nugget in my tooth, if it’s a symbol like any other symbol, what’ll it take to expose it so we could do func[Symbol.homeObject]?

To answer that question let’s look at how other symbols are declared and copy them. While we’re doing that remember the line of code we saw above: isolate->heap()->home_object_symbol() . We don’t have to understand what it does, just its overall feel. We’ll pick a less used symbol so we’ll have an easier time searching, unscopables , and narrow the search down to only search in v8, and exclude tests and javascript files:

Cool, only 6 results, very easy to go over all of them. Let’s take a looksie:

  • api.cc doesn’t look related, we’re looking for implementation andGetUnscopables ain’t it.
  • v8.h same, only declares GetUnscopables.
  • All results in contexts.cc are related to UnscopablesLookup .
  • ChangeLog. Fascinating indeed, but not for our purposes.
  • heap-symbols.h looks just about right. Remember the line above? We got a reference to home_object_symbol by getting some heap. Let’s open that in another tab and look at the final result.
  • bootstrapper.cc What a weird name, but a call to InstallConstant inside a function called InitializeGlobal is enough to interest me any day of the week! Open that in yet another tab.

So let’s see what that heap is about:

#define PRIVATE_SYMBOL_LIST(V)              \
V(array_iteration_kind_symbol) \
V(array_iterator_next_symbol) \
// ...
V(home_object_symbol) \
// ...
V(uninitialized_symbol)

#define PUBLIC_SYMBOL_LIST(V) \
V(async_iterator_symbol, Symbol.asyncIterator) \
V(iterator_symbol, Symbol.iterator) \
// ...
V(to_primitive_symbol, Symbol.toPrimitive) \
V(unscopables_symbol, Symbol.unscopables)

Wanderin’ into our town are two lists, a PRIVATE and a PUBLIC symbol list. We see that the dear unscopables is in the PUBLIC list but our protégé home_object is under the PRIVATE list! It’s time to shake its shy demeanour and expose him to the world in this episode of “Introvert to Extrovert: EXTREME Symbol Makeovers”:

Just moving the symbol down to the PUBLIC list and giving it that weird second argument, the PUBLIC list now looks like:

#define PUBLIC_SYMBOL_LIST(V)                    \
V(async_iterator_symbol, Symbol.asyncIterator) \
V(iterator_symbol, Symbol.iterator) \
V(intl_fallback_symbol, IntlFallback) \
V(match_symbol, Symbol.match) \
V(replace_symbol, Symbol.replace) \
V(search_symbol, Symbol.search) \
V(species_symbol, Symbol.species) \
V(split_symbol, Symbol.split) \
V(to_primitive_symbol, Symbol.toPrimitive) \
V(home_object_symbol, Symbol.homeObject) \
V(unscopables_symbol, Symbol.unscopables)

Cool. However, we only made this symbol “public”, whatever that means, we still have to expose it the Symbol function somehow! Remember that InstallConstant result we saw above? Let’s look at that code.

// Install well-known symbols.
InstallConstant(isolate, symbol_fun, "hasInstance",
factory->has_instance_symbol());
InstallConstant(isolate, symbol_fun, "isConcatSpreadable",
factory->is_concat_spreadable_symbol());
// ...
InstallConstant(isolate, symbol_fun, "unscopables",
factory->unscopables_symbol());

okay…those are all the properties of Symbol. Let’s add our own!

And we’re good to go!

Let’s Play v8

Compiling v8 is one of those things at on the one hand isn’t difficult, but on the other is a bit of a drag at first, so let’s walk through those steps. I’ll be using Linux, the instructions for OSX should be pretty much the same (wooo shells!), and the documentation links provide install instructions for Windows.

Anyway! Google has a lot of big, inter-connected projects and their own suite of tools to manage such projects called depot_tools. It contains helpers to clone repos, keep 3rd-party dependencies up to date, create issues and patches, etc etc. They also contain the necessary build system (ninja) and makefile generator (gn). Who said software development isn’t fun?

We’ll begin by installing depot_tools:

$ mkdir awesome-v8-playground && cd awesome-v8-playground
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
$ export PATH=”$(pwd)/depot_tools”:”$PATH”
$ which gn
/home/zirak/awesome-v8-playground/depot_tools/gn

Coolio. If you’re planning on working on v8 or chromium a lot, I recommend you put that export in your favourite shell configuration file. Let’s look at what that brought us:

$ ls depot_tools | wc
165

Yikes, that’s quite a lot. The ones we’ll use are:

  • fetch: Used for downloading Chromium-family projects.
  • gclient: Dependency manager, sort of a submodules wrapper/replacement
  • ninja: Build system, a make alternative
  • gn: makefile (rather, ninja-file) generator

Now let’s fetch the v8 source:

$ fetch v8
Running: gclient root
Running: gclient config --spec 'solutions = [
{
"url": "https://chromium.googlesource.com/v8/v8.git",
"managed": False,
"name": "v8",
"deps_file": "DEPS",
"custom_deps": {},
},
]
'
Running: gclient sync --with_branch_heads
... take time to contemplate your existence ...
... did you contemplate? maybe it'll go faster if you do ...
... maybe reconsider software dev. pick up the viola ...
... the viola is a great instrument ...
... such a tender sound, and a greater amplitude than the violin ...
... hey remember that guy you saw at the gas station that one time who looked kinda weird and preoccupied? wonder what's up with him ...
... maybe he's a viola player wondering what's up with violottas ...
... oh look it finished ...
Running: git submodule foreach 'git config -f $toplevel/.git/config submodule.$name.ignore all'
Running: git config --add remote.origin.fetch '+refs/tags/*:refs/tags/*'
Running: git config diff.ignoreSubmodules all

Viola! Now let’s do the build-tool dance:

$ gclient sync
... maybe pmj are hiring...
$ tools/dev/v8gen.py x64.release

The next step’s the compilation. Let’s first do a clean build before modifying the source: (note to make aficionados: you don’t need to pass -j, ninja is smart)

$ ninja -C out.gn/x64.release/
ninja: Entering directory `out.gn/x64.release/'
[88/1475] CXX obj/src/inspector/inspector/violas.o

If you’re on a x86 system you should reconsider some life choices and replace x64.release in the above several commands with ia32.release , i.e.

# Note: only for x86 compilations
$ tools/dev/v8gen.py ia32.release
$ ninja -C out.gn/ia32.release/

We can also see a list of possible compilation targets:

$ tools/dev/v8gen.py list
arm.debug
arm.optdebug
arm.release
arm64.debug
arm64.optdebug
arm64.release
ia32.debug
ia32.optdebug
ia32.release
mips64el.debug
mips64el.optdebug
mips64el.release
...

We’re compiling to release since we’re not going to debug v8’s native code. If you’re adventurous and want debug symbols, in addition to compiling to a debug target I suggest passing the is_component_build flag to gn to speed up each re-compilation. Skip the v8gen.py step and run something like the following:

# Note: This is for debug builds. I'll get on with the regular
# article after this!
$ gn gen out.gn/awesome --args='is_debug=true target_cpu="x64" is_component_build=true'
$ ninja -C out.gn/awesome/

It finished compiling!

Hurray! Compiling v8 gives us some cool toys to play with, first and foremost d8, a basic javascript repl:

$ out.gn/x64.release/
V8 version 5.9.0 (candidate)
d8> 4 + 4
8
d8>

Sexy. Let’s apply our patches (bootsrapper.cc, heap-symbols.h). Download them to a local directory and git apply:

$ git apply heap-symbols.h.diff
$ git apply bootstrapper.cc.diff

Note that apply is very very sensitive and omitting parts of the diff may offend it and cause it to storm out of the room in tears. Also, I expect that these diffs will break any day now so don’t rely on them: If apply throws a fit it’s okay, no reason to give up, you can do these edits manually.

Let’s recompile and see what’s what:

$ ninja -C out.gn/x64.release/
ninja: Entering directory `out.gn/x64.release/’
[12/751] CXX obj/v8_base/life-regret-analysis.o

While it’s compiling, let’s take a look back at InitializeGlobal in bootstrapper.cc. I found that it’s a nice play to look at occasionally and learn about new or obscure features. For instance did you know about Number.isSafeInteger ? I certainly didn’t! Apparently it’s part of ES2015 though (mdn, spec). Poke around this file in general, it’s fairly interesting.

Oh look, it finished compiling. Let’s take a quick looksie…

$ out.gn/x64.release/d8
V8 version 5.9.0 (candidate)
d8> Symbol.homeObject
Symbol(Symbol.homeObject)

SUCCESS! Time to try out this bad boy. Remember, super is supposedly implemented as sort of

Object.getPrototypeOf(func.[[HomeObject]])

So HomeObject should be a property on functions.

d8> var o = { f() {} };
d8> o.f[Symbol.homeObject]
undefined

huh, that’s weird. Let’s play a bit more, maybe we missed something? Let’s try again. Quit d8, and run again with cheat codes:

$ out.gn/x64.release/d8 --allow-natives-syntax 
V8 version 5.9.0 (candidate)
d8> %HomeObjectSymbol() === Symbol.homeObject // sanity
true
d8> var o = { f() {} };
undefined
d8> o.f[%HomeObjectSymbol()]
undefined

Looks like we have a deeper problem. Our Symbol.homeObject patch works but it seems like o.f doesn’t have a HomeObject! Take a few minutes to see if you can find out why.

I’ll wait.

(。◕‿‿◕。)

༼ʘ̚ل͜ʘ̚༽

So! What could go wrong? I started out by writing a few lines which have to use HomeObject:

d8> var parent = { x: 4 };
d8> var child = { __proto__: parent, whatsX() { return super.x; } };
d8> child.whatsX[Symbol.homeObject]
{whatsX: whatsX() { return super.x; }}

That works, so the difference seems to stem from using super. Oh wait, remember our very first search for HomeObject?

See that NeedsHomeObject method? Sounds pretty important right now! Let’s check it out:

static bool NeedsHomeObject(Expression* expr) {
return FunctionLiteral::NeedsHomeObject(expr);
}

uunnghh, indirections…click on NeedsHomeObject and it for some reason brings up almost to the right place:

bool FunctionLiteral::NeedsHomeObject(Expression* expr) {
if (expr == nullptr || !expr->IsFunctionLiteral()) return false;
DCHECK_NOT_NULL(expr->AsFunctionLiteral()->scope());
return expr->AsFunctionLiteral()->scope()->NeedsHomeObject();
}

okay, click on NeedsHomeObject again:

// src/scopes.h
bool
NeedsHomeObject() const {
return scope_uses_super_property_ ||
(inner_scope_calls_eval_ && (IsConciseMethod(function_kind()) ||
IsAccessorFunction(function_kind()) ||
IsClassConstructor(function_kind())));
}

Jackpot! medium may have messed up some of the indentation but this looks promising. v8 being an optimisation-happy critter only wants to put HomeObject on functions which need it. Sneaky critter.

An interesting tangent I won’t go into is discovering where NeedsHomeObject is used and how it affects functions. If you want to go down that road, I personally suggest you go to the original callsite and click on the method name. That should bring up the list of XRefs (cross-references) to the method:

ooohhh, fancy

(A thing to note about chromium’s source browser (besides how awesome it is) is that it works by indexing one compilation target, I suspect the x64 linux one. That means some things may not work so great, like checking XRefs to Windows-only functions or, in this case, XRefs to the codegen for non-x64 architectures like mips. We don’t necessarily care right now, just a thing to remember.)

For all others who value their sanity and are oh-so-eager to continue this treacherous journey, delve onwards!

Patching NeedsHomeObject

It’s a simple diff, really:

Save locally and do the patch/recompile dance:

$ git apply scopes.h.diff
$ ninja -C out.gn/x64.release/
ninja: Entering directory `out.gn/x64.release/'
[175/175] STAMP obj/going.postal.aarrgghhh
$ out.gn/x64.release/d8
V8 version 5.9.0 (candidate)
d8> var o = { f() {} };
undefined
d8> o.f[Symbol.homeObject]
{f: f() {}}
d8>

̿̿ ̿̿ ̿̿ ̿’̿’\̵͇̿̿\з= ( ▀ ͜͞ʖ▀) =ε/̵͇̿̿/’̿’̿ ̿ ̿̿ ̿̿ ̿̿

Dance

What did we learn today?

  • The entirety of Chromium’s source code is indexed and easily accessible via https://cs.chromium.org/
  • We can understand how specific sections of code work without knowing exactly how or why they work by using guesstimates and googling around
  • Additionally, we can use the knowledge found inside the source code to modify existing pieces
  • Tests are an amazing treasure trove of knowledge
  • Compiling v8 isn’t that bad
  • ASCII faces are fun

Finally, the complete git diff :

And we’ve got an exposed homeObject . I call that a win.

More by Zirak

Topics of interest

More Related Stories