As I read more of the spec, I became more intrigued about [[[HomeObject]]](https://tc39.github.io/ecma262/#table-16).
[[HomeObject]]
is used by the super
keyword:
let parent = {x: 'parent'};let child = {x: 'child',papaX() { return super.x; }};
child.papaX(); // undefinedObject.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!
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?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](https://cs.chromium.org/chromium/src/v8/src/runtime/runtime-classes.cc?l=96&rcl=06d36330e481d098c19fe047809d45955837ee46)
inside of [runtime-classes.cc](https://cs.chromium.org/chromium/src/v8/src/runtime/runtime-classes.cc?l=96&rcl=06d36330e481d098c19fe047809d45955837ee46)
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](https://cs.chromium.org/chromium/src/v8/src/runtime/runtime-classes.cc?l=96&ct=xref_jump_to_def&gsn=isolate)->[heap](https://cs.chromium.org/chromium/src/v8/src/isolate.h?l=856&ct=xref_jump_to_def&gsn=heap)()->[home_object_symbol](https://cs.chromium.org/chromium/src/v8/src/heap/heap-inl.h?l=139&ct=xref_jump_to_def&gsn=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](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/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
.contexts.cc
are related to UnscopablesLookup
.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!
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 | wc165
Yikes, that’s quite a lot. The ones we’ll use are:
make
alternativeNow let’s fetch the v8 source:
$ fetch v8Running: gclient rootRunning: 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 listarm.debugarm.optdebugarm.releasearm64.debugarm64.optdebugarm64.releaseia32.debugia32.optdebugia32.releasemips64el.debugmips64el.optdebugmips64el.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/
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 + 48d8>
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](https://cs.chromium.org/chromium/src/v8/src/bootstrapper.cc?l=1174&rcl=4e3e384275b567b0b518412e7c78cb25dd3f1782)
in [bootstrapper.cc](https://cs.chromium.org/chromium/src/v8/src/bootstrapper.cc?l=1174&rcl=4e3e384275b567b0b518412e7c78cb25dd3f1782)
. 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](https://cs.chromium.org/chromium/src/v8/src/bootstrapper.cc?l=1539&rcl=4e3e384275b567b0b518412e7c78cb25dd3f1782)
? 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/d8V8 version 5.9.0 (candidate)d8> Symbol.homeObjectSymbol(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-syntaxV8 version 5.9.0 (candidate)d8> %HomeObjectSymbol() === Symbol.homeObject // sanitytrued8> var o = { f() {} };undefinedd8> 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 || ->IsFunctionLiteral()) return false;DCHECK_NOT_NULL(expr->AsFunctionLiteral()->scope());return expr->AsFunctionLiteral()->scope()->NeedsHomeObject();}
okay, click on [NeedsHomeObject](https://cs.chromium.org/chromium/src/v8/src/ast/scopes.h?l=643&rcl=f8189977d2cf40ce3c0b1e1d10f0717c2e2a3e7a)
again:
// src/scopes.hbool 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!
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/d8V8 version 5.9.0 (candidate)d8> var o = { f() {} };undefinedd8> o.f[Symbol.homeObject]{f: f() {}}d8>
̿̿ ̿̿ ̿̿ ̿’̿’\̵͇̿̿\з= ( ▀ ͜͞ʖ▀) =ε/̵͇̿̿/’̿’̿ ̿ ̿̿ ̿̿ ̿̿
What did we learn today?
Finally, the complete git diff
:
And we’ve got an exposed homeObject
. I call that a win.