Debug check failed: !isolate->builtins()->is_initialized().
April 20, 2022 ยท View on GitHub
Learning Google V8
The sole purpose of this project is to aid me in leaning Google's V8 JavaScript engine.
Contents
- Introduction
- Address
- TaggedImpl
- Object
- Handle
- FunctionTemplate
- ObjectTemplate
- Small Integers
- String types
- Roots
- Heap
- Builtins
- Compiler pipeline
- CodeStubAssembler
- Torque
- WebAssembly
- Promises
- Snapshots
- V8 Build artifacts
- V8 Startup walkthrough
- Building V8
- Contributing a change
- Debugging
- Building chromium
- Goma chromium
- EcmaScript notes
- GN notes
Isolate
An Isolate is an independant copy of the V8 runtime which includes its own heap. Two different Isolates can run in parallel and can be seen as entirely different sandboxed instances of a V8 runtime.
Context
To allow separate JavaScript applications to run in the same isolate a context must be specified for each one. This is to avoid them interfering with each other, for example by changing the builtin objects provided.
Template
This is the super class of both ObjecTemplate and FunctionTemplate. Remember that in JavaScript a function can have fields just like objects.
class V8_EXPORT Template : public Data {
public:
void Set(Local<Name> name, Local<Data> value,
PropertyAttribute attributes = None);
void SetPrivate(Local<Private> name, Local<Data> value,
PropertyAttribute attributes = None);
V8_INLINE void Set(Isolate* isolate, const char* name, Local<Data> value);
void SetAccessorProperty(
Local<Name> name,
Local<FunctionTemplate> getter = Local<FunctionTemplate>(),
Local<FunctionTemplate> setter = Local<FunctionTemplate>(),
PropertyAttribute attribute = None,
AccessControl settings = DEFAULT);
The Set function can be used to have an name and a value set on an instance
created from this template.
The SetAccessorProperty is for properties that are get/set using functions.
enum PropertyAttribute {
/** None. **/
None = 0,
/** ReadOnly, i.e., not writable. **/
ReadOnly = 1 << 0,
/** DontEnum, i.e., not enumerable. **/
DontEnum = 1 << 1,
/** DontDelete, i.e., not configurable. **/
DontDelete = 1 << 2
};
enum AccessControl {
DEFAULT = 0,
ALL_CAN_READ = 1,
ALL_CAN_WRITE = 1 << 1,
PROHIBITS_OVERWRITING = 1 << 2
};
ObjectTemplate
These allow you to create JavaScript objects without a dedicated constructor. When an instance is created using an ObjectTemplate the new instance will have the properties and functions configured on the ObjectTemplate.
This would be something like:
const obj = {};
This class is declared in include/v8.h and extends Template:
class V8_EXPORT ObjectTemplate : public Template {
...
}
class V8_EXPORT Template : public Data {
...
}
class V8_EXPORT Data {
private:
Data();
};
We create an instance of ObjectTemplate and we can add properties to it that
all instance created using this ObjectTemplate instance will have. This is done
by calling Set which is member of the Template class. You specify a
LocalName is a superclass for Symbol and String
which can be both be used as names for a property.
The implementation for Set can be found in src/api/api.cc:
void Template::Set(v8::Local<Name> name, v8::Local<Data> value, v8::PropertyAttribute attribute) {
...
i::ApiNatives::AddDataProperty(isolate, templ, Utils::OpenHandle(*name),
value_obj,
static_cast<i::PropertyAttributes>(attribute));
}
There is an example in objecttemplate_test.cc
FunctionTemplate
Is a template that is used to create functions and like ObjectTemplate it inherits from Template:
class V8_EXPORT FunctionTemplate : public Template {
}
Rememeber that a function in javascript can have properties just like object.
There is an example in functiontemplate_test.cc
An instance of a function template can be created using:
Local<FunctionTemplate> ft = FunctionTemplate::New(isolate_, function_callback, data);
Local<Function> function = ft->GetFunction(context).ToLocalChecked();
And the function can be called using:
MaybeLocal<Value> ret = function->Call(context, recv, 0, nullptr);
Function::Call can be found in src/api/api.cc:
bool has_pending_exception = false;
auto self = Utils::OpenHandle(this);
i::Handle<i::Object> recv_obj = Utils::OpenHandle(*recv);
i::Handle<i::Object>* args = reinterpret_cast<i::Handle<i::Object>*>(argv);
Local<Value> result;
has_pending_exception = !ToLocal<Value>(
i::Execution::Call(isolate, self, recv_obj, argc, args), &result);
Notice that the return value of Call which is a MaybeHandle<Object> will be
passed to ToLocalapi.h:
template <class T>
inline bool ToLocal(v8::internal::MaybeHandle<v8::internal::Object> maybe,
Local<T>* local) {
v8::internal::Handle<v8::internal::Object> handle;
if (maybe.ToHandle(&handle)) {
*local = Utils::Convert<v8::internal::Object, T>(handle);
return true;
}
return false;
So lets take a look at Execution::Call which can be found in execution/execution.cc
and it calls:
return Invoke(isolate, InvokeParams::SetUpForCall(isolate, callable, receiver, argc, argv));
SetUpForCall will return an InvokeParams.
TODO: Take a closer look at InvokeParams.
V8_WARN_UNUSED_RESULT MaybeHandle<Object> Invoke(Isolate* isolate,
const InvokeParams& params) {
Handle<Object> receiver = params.is_construct
? isolate->factory()->the_hole_value()
: params.receiver;
In our case is_construct is false as we are not using new and the receiver,
the this in the function should be set to the receiver that we passed in. After
that we have Builtins::InvokeApiFunction
auto value = Builtins::InvokeApiFunction(
isolate, params.is_construct, function, receiver, params.argc,
params.argv, Handle<HeapObject>::cast(params.new_target));
result = HandleApiCallHelper<false>(isolate, function, new_target,
fun_data, receiver, arguments);
api-arguments-inl.h has:
FunctionCallbackArguments::Call(CallHandlerInfo handler) {
...
ExternalCallbackScope call_scope(isolate, FUNCTION_ADDR(f));
FunctionCallbackInfo<v8::Value> info(values_, argv_, argc_);
f(info);
return GetReturnValue<Object>(isolate);
}
The call to f(info) is what invokes the callback, which is just a normal function call.
Back in HandleApiCallHelper we have:
Handle<Object> result = custom.Call(call_data);
RETURN_EXCEPTION_IF_SCHEDULED_EXCEPTION(isolate, Object);
RETURN_EXCEPTION_IF_SCHEDULED_EXCEPTION expands to:
Handle<Object> result = custom.Call(call_data);
do {
Isolate* __isolate__ = (isolate);
((void) 0);
if (__isolate__->has_scheduled_exception()) {
__isolate__->PromoteScheduledException();
return MaybeHandle<Object>();
}
} while (false);
Notice that if there was an exception an empty object is returned.
Later in Invoke in execution.cca:
auto value = Builtins::InvokeApiFunction(
isolate, params.is_construct, function, receiver, params.argc,
params.argv, Handle<HeapObject>::cast(params.new_target));
bool has_exception = value.is_null();
if (has_exception) {
if (params.message_handling == Execution::MessageHandling::kReport) {
isolate->ReportPendingMessages();
}
return MaybeHandle<Object>();
} else {
isolate->clear_pending_message();
}
return value;
Looking at this is looks like passing back an empty object will cause an exception to be triggered?
Address
Address can be found in include/v8-internal.h:
typedef uintptr_t Address;
uintptr_t is an optional type specified in cstdint and is capable of storing
a data pointer. It is an unsigned integer type that any valid pointer to void
can be converted to this type (and back).
TaggedImpl
This class is declared in `src/objects/tagged-impl.h and has a single private member which is declared as:
public
constexpr StorageType ptr() const { return ptr_; }
private:
StorageType ptr_;
An instance can be created using:
i::TaggedImpl<i::HeapObjectReferenceType::STRONG, i::Address> tagged{};
Storage type can also be Tagged_t which is defined in globals.h:
using Tagged_t = uint32_t;
It looks like it can be a different value when using pointer compression.
See tagged_test.cc for an example.
Object
This class extends TaggedImpl:
class Object : public TaggedImpl<HeapObjectReferenceType::STRONG, Address> {
An Object can be created using the default constructor, or by passing in an
Address which will delegate to TaggedImpl constructors. Object itself does
not have any members (apart from ptr_ which is inherited from TaggedImpl that is).
So if we create an Object on the stack this is like a pointer/reference to
an object:
+------+
|Object|
|------|
|ptr_ |---->
+------+
Now, ptr_ is a StorageType so it could be a Smi in which case it would just
contains the value directly, for example a small integer:
+------+
|Object|
|------|
| 18 |
+------+
See object_test.cc for an example.
ObjectSlot
i::Object obj{18};
i::FullObjectSlot slot{&obj};
+----------+ +---------+
|ObjectSlot| | Object |
|----------| |---------|
| address | ---> | 18 |
+----------+ +---------+
See objectslot_test.cc for an example.
Maybe
A Maybe is like an optional which can either hold a value or nothing.
template <class T>
class Maybe {
public:
V8_INLINE bool IsNothing() const { return !has_value_; }
V8_INLINE bool IsJust() const { return has_value_; }
...
private:
bool has_value_;
T value_;
}
I first thought that name Just was a little confusing but if you read this
like:
bool cond = true;
Maybe<int> maybe = cond ? Just<int>(10) : Nothing<int>();
I think it makes more sense. There are functions that check if the Maybe is
nothing and crash the process if so. You can also check and return the value
by using FromJust.
The usage of Maybe is where api calls can fail and returning Nothing is a way of signaling this.
See maybe_test.cc for an example.
MaybeLocal
template <class T>
class MaybeLocal {
public:
V8_INLINE MaybeLocal() : val_(nullptr) {}
V8_INLINE Local<T> ToLocalChecked();
V8_INLINE bool IsEmpty() const { return val_ == nullptr; }
template <class S>
V8_WARN_UNUSED_RESULT V8_INLINE bool ToLocal(Local<S>* out) const {
out->val_ = IsEmpty() ? nullptr : this->val_;
return !IsEmpty();
}
private:
T* val_;
ToLocalChecked will crash the process if val_ is a nullptr. If you want to
avoid a crash one can use ToLocal.
See maybelocal_test.cc for an example.
Data
Is the super class of all objects that can exist the V8 heap:
class V8_EXPORT Data {
private:
Data();
};
Value
Value extends Data and adds a number of methods that check if a Value
is of a certain type, like IsUndefined(), IsNull, IsNumber etc.
It also has useful methods to convert to a Local
V8_WARN_UNUSED_RESULT MaybeLocal<Number> ToNumber(Local<Context> context) const;
V8_WARN_UNUSED_RESULT MaybeLocal<String> ToNumber(Local<String> context) const;
...
Handle
A Handle is similar to a Object and ObjectSlot in that it also contains
an Address member (called location_ and declared in HandleBase), but with the
difference is that Handles acts as a layer of abstraction and can be relocated
by the garbage collector.
Can be found in src/handles/handles.h.
class HandleBase {
...
protected:
Address* location_;
}
template <typename T>
class Handle final : public HandleBase {
...
}
+----------+ +--------+ +---------+
| Handle | | Object | | int |
|----------| +-----+ |--------| |---------|
|*location_| ---> |&ptr_| --> | ptr_ | -----> | 5 |
+----------+ +-----+ +--------+ +---------+
(gdb) p handle
\$8 = {<v8::internal::HandleBase> = {location_ = 0x7ffdf81d60c0}, <No data fields>}
Notice that location_ contains a pointer:
(gdb) p /x *(int*)0x7ffdf81d60c0
\$9 = 0xa9d330
And this is the same as the value in obj:
(gdb) p /x obj.ptr_
\$14 = 0xa9d330
And we can access the int using any of the pointers:
(gdb) p /x *value
\$16 = 0x5
(gdb) p /x *obj.ptr_
\$17 = 0x5
(gdb) p /x *(int*)0x7ffdf81d60c0
\$18 = 0xa9d330
(gdb) p /x *(*(int*)0x7ffdf81d60c0)
\$19 = 0x5
See handle_test.cc for an example.
HandleScope
Contains a number of Local/Handle's (think pointers to objects but is managed by V8) and will take care of deleting the Local/Handles for us. HandleScopes are stack allocated
When ~HandleScope is called all handles created within that scope are removed from the stack maintained by the HandleScope which makes objects to which the handles point being eligible for deletion from the heap by the GC.
A HandleScope only has three members:
internal::Isolate* isolate_;
internal::Address* prev_next_;
internal::Address* prev_limit_;
Lets take a closer look at what happens when we construct a HandleScope:
v8::HandleScope handle_scope{isolate_};
The constructor call will end up in src/api/api.cc and the constructor simply
delegates to Initialize:
HandleScope::HandleScope(Isolate* isolate) { Initialize(isolate); }
void HandleScope::Initialize(Isolate* isolate) {
i::Isolate* internal_isolate = reinterpret_cast<i::Isolate*>(isolate);
...
i::HandleScopeData* current = internal_isolate->handle_scope_data();
isolate_ = internal_isolate;
prev_next_ = current->next;
prev_limit_ = current->limit;
current->level++;
}
Every v8::internal::Isolate has member of type HandleScopeData:
HandleScopeData* handle_scope_data() { return &handle_scope_data_; }
HandleScopeData handle_scope_data_;
HandleScopeData is a struct defined in src/handles/handles.h:
struct HandleScopeData final {
Address* next;
Address* limit;
int level;
int sealed_level;
CanonicalHandleScope* canonical_scope;
void Initialize() {
next = limit = nullptr;
sealed_level = level = 0;
canonical_scope = nullptr;
}
};
Notice that there are two pointers (Address*) to next and a limit. When a HandleScope is Initialized the current handle_scope_data will be retrieved from the internal isolate. The HandleScope instance that is getting created stores the next/limit pointers of the current isolate so that they can be restored when this HandleScope is closed (see CloseScope).
So with a HandleScope created, how does a Local
When a Local
Handle<Struct> str = handle(Struct::cast(result), isolate());
This will land in the constructor Handle
template <typename T>
Handle<T>::Handle(T object, Isolate* isolate): HandleBase(object.ptr(), isolate) {}
HandleBase::HandleBase(Address object, Isolate* isolate)
: location_(HandleScope::GetHandle(isolate, object)) {}
Notice that object.ptr() is used to pass the Address to HandleBase.
And also notice that HandleBase sets its location_ to the result of HandleScope::GetHandle.
Address* HandleScope::GetHandle(Isolate* isolate, Address value) {
DCHECK(AllowHandleAllocation::IsAllowed());
HandleScopeData* data = isolate->handle_scope_data();
CanonicalHandleScope* canonical = data->canonical_scope;
return canonical ? canonical->Lookup(value) : CreateHandle(isolate, value);
}
Which will call CreateHandle in this case and this function will retrieve the
current isolate's handle_scope_data:
HandleScopeData* data = isolate->handle_scope_data();
Address* result = data->next;
if (result == data->limit) {
result = Extend(isolate);
}
In this case both next and limit will be 0x0 so Extend will be called. Extend will also get the isolates handle_scope_data and check the current level and after that get the isolates HandleScopeImplementer:
HandleScopeImplementer* impl = isolate->handle_scope_implementer();
HandleScopeImplementer is declared in src/api/api.h
HandleScope:CreateHandle will get the handle_scope_data from the isolate:
Address* HandleScope::CreateHandle(Isolate* isolate, Address value) {
HandleScopeData* data = isolate->handle_scope_data();
if (result == data->limit) {
result = Extend(isolate);
}
// Update the current next field, set the value in the created handle,
// and return the result.
data->next = reinterpret_cast<Address*>(reinterpret_cast<Address>(result) + sizeof(Address));
*result = value;
return result;
}
Notice that data->next is set to the address passed in + the size of an
Address.
The destructor for HandleScope will call CloseScope. See handlescope_test.cc for an example.
EscapableHandleScope
Local handles are located on the stack and are deleted when the appropriate destructor is called. If there is a local HandleScope then it will take care of this when the scope returns. When there are no references left to a handle it can be garbage collected. This means if a function has a HandleScope and wants to return a handle/local it will not be available after the function returns. This is what EscapableHandleScope is for, it enable the value to be placed in the enclosing handle scope to allow it to survive. When the enclosing HandleScope goes out of scope it will be cleaned up.
class V8_EXPORT EscapableHandleScope : public HandleScope {
public:
explicit EscapableHandleScope(Isolate* isolate);
V8_INLINE ~EscapableHandleScope() = default;
template <class T>
V8_INLINE Local<T> Escape(Local<T> value) {
internal::Address* slot = Escape(reinterpret_cast<internal::Address*>(*value));
return Local<T>(reinterpret_cast<T*>(slot));
}
template <class T>
V8_INLINE MaybeLocal<T> EscapeMaybe(MaybeLocal<T> value) {
return Escape(value.FromMaybe(Local<T>()));
}
private:
...
internal::Address* escape_slot_;
};
From api.cc
EscapableHandleScope::EscapableHandleScope(Isolate* v8_isolate) {
i::Isolate* isolate = reinterpret_cast<i::Isolate*>(v8_isolate);
escape_slot_ = CreateHandle(isolate, i::ReadOnlyRoots(isolate).the_hole_value().ptr());
Initialize(v8_isolate);
}
So when an EscapableHandleScope is created it will create a handle with the
hole value and store it in the escape_slot_ which is of type Address. This
Handle will be created in the current HandleScope, and EscapableHandleScope
can later set a value for that pointer/address which it want to be escaped.
Later when that HandleScope goes out of scope it will be cleaned up.
It then calls Initialize just like a normal HandleScope would.
i::Address* HandleScope::CreateHandle(i::Isolate* isolate, i::Address value) {
return i::HandleScope::CreateHandle(isolate, value);
}
From handles-inl.h:
Address* HandleScope::CreateHandle(Isolate* isolate, Address value) {
DCHECK(AllowHandleAllocation::IsAllowed());
HandleScopeData* data = isolate->handle_scope_data();
Address* result = data->next;
if (result == data->limit) {
result = Extend(isolate);
}
// Update the current next field, set the value in the created handle,
// and return the result.
DCHECK_LT(reinterpret_cast<Address>(result),
reinterpret_cast<Address>(data->limit));
data->next = reinterpret_cast<Address*>(reinterpret_cast<Address>(result) +
sizeof(Address));
*result = value;
return result;
}
When Escape is called the following happens (v8.h):
template <class T>
V8_INLINE Local<T> Escape(Local<T> value) {
internal::Address* slot = Escape(reinterpret_cast<internal::Address*>(*value));
return Local<T>(reinterpret_cast<T*>(slot));
}
An the EscapeableHandleScope::Escape (api.cc):
i::Address* EscapableHandleScope::Escape(i::Address* escape_value) {
i::Heap* heap = reinterpret_cast<i::Isolate*>(GetIsolate())->heap();
Utils::ApiCheck(i::Object(*escape_slot_).IsTheHole(heap->isolate()),
"EscapableHandleScope::Escape", "Escape value set twice");
if (escape_value == nullptr) {
*escape_slot_ = i::ReadOnlyRoots(heap).undefined_value().ptr();
return nullptr;
}
*escape_slot_ = *escape_value;
return escape_slot_;
}
If the escape_value is null, the escape_slot that is a pointer into the
parent HandleScope is set to the undefined_value() instead of the hole value
which is was previously, and nullptr will be returned. This returned
address/pointer will then be returned after being casted to T*.
Next, we take a look at what happens when the EscapableHandleScope goes out of
scope. This will call HandleScope::~HandleScope which makes sense as any other
Local handles should be cleaned up.
Escape copies the value of its argument into the enclosing scope, deletes alli
its local handles, and then gives back the new handle copy which can safely be
returned.
HeapObject
TODO:
Local
Has a single member val_ which is of type pointer to T:
template <class T> class Local {
...
private:
T* val_
}
Notice that this is a pointer to T. We could create a local using:
v8::Local<v8::Value> empty_value;
So a Local contains a pointer to type T. We can access this pointer using
operator-> and operator*.
We can cast from a subtype to a supertype using Local::Cast:
v8::Local<v8::Number> nr = v8::Local<v8::Number>(v8::Number::New(isolate_, 12));
v8::Local<v8::Value> val = v8::Local<v8::Value>::Cast(nr);
And there is also the
v8::Local<v8::Value> val2 = nr.As<v8::Value>();
See local_test.cc for an example.
PrintObject
Using _v8_internal_Print_Object from c++:
$ nm -C libv8_monolith.a | grep Print_Object
0000000000000000 T _v8_internal_Print_Object(void*)
Notice that this function does not have a namespace. We can use this as:
extern void _v8_internal_Print_Object(void* object);
_v8_internal_Print_Object(*((v8::internal::Object**)(*global)));
Lets take a closer look at the above:
v8::internal::Object** gl = ((v8::internal::Object**)(*global));
We use the dereference operator to get the value of a Local (*global), which is
just of type T*, a pointer to the type the Local:
template <class T>
class Local {
...
private:
T* val_;
}
We are then casting that to be of type pointer-to-pointer to Object.
gl** Object* Object
+-----+ +------+ +-------+
| |----->| |----->| |
+-----+ +------+ +-------+
An instance of v8::internal::Object only has a single data member which is a
field named ptr_ of type Address:
src/objects/objects.h:
class Object : public TaggedImpl<HeapObjectReferenceType::STRONG, Address> {
public:
constexpr Object() : TaggedImpl(kNullAddress) {}
explicit constexpr Object(Address ptr) : TaggedImpl(ptr) {}
#define IS_TYPE_FUNCTION_DECL(Type) \
V8_INLINE bool Is##Type() const; \
V8_INLINE bool Is##Type(const Isolate* isolate) const;
OBJECT_TYPE_LIST(IS_TYPE_FUNCTION_DECL)
HEAP_OBJECT_TYPE_LIST(IS_TYPE_FUNCTION_DECL)
IS_TYPE_FUNCTION_DECL(HashTableBase)
IS_TYPE_FUNCTION_DECL(SmallOrderedHashTable)
#undef IS_TYPE_FUNCTION_DECL
V8_INLINE bool IsNumber(ReadOnlyRoots roots) const;
}
Lets take a look at one of these functions and see how it is implemented. For example in the OBJECT_TYPE_LIST we have:
#define OBJECT_TYPE_LIST(V) \
V(LayoutDescriptor) \
V(Primitive) \
V(Number) \
V(Numeric)
So the object class will have a function that looks like:
inline bool IsNumber() const;
inline bool IsNumber(const Isolate* isolate) const;
And in src/objects/objects-inl.h we will have the implementations:
bool Object::IsNumber() const {
return IsHeapObject() && HeapObject::cast(*this).IsNumber();
}
IsHeapObject is defined in TaggedImpl:
constexpr inline bool IsHeapObject() const { return IsStrong(); }
constexpr inline bool IsStrong() const {
#if V8_HAS_CXX14_CONSTEXPR
DCHECK_IMPLIES(!kCanBeWeak, !IsSmi() == HAS_STRONG_HEAP_OBJECT_TAG(ptr_));
#endif
return kCanBeWeak ? HAS_STRONG_HEAP_OBJECT_TAG(ptr_) : !IsSmi();
}
The macro can be found in src/common/globals.h:
#define HAS_STRONG_HEAP_OBJECT_TAG(value) \
(((static_cast<i::Tagged_t>(value) & ::i::kHeapObjectTagMask) == \
::i::kHeapObjectTag))
So we are casting ptr_ which is of type Address into type Tagged_t which
is defined in src/common/global.h and can be different depending on if compressed
pointers are used or not. If they are not supported it is the same as Address:
using Tagged_t = Address;
src/objects/tagged-impl.h:
template <HeapObjectReferenceType kRefType, typename StorageType>
class TaggedImpl {
StorageType ptr_;
}
The HeapObjectReferenceType can be either WEAK or STRONG. And the storage type
is Address in this case. So Object itself only has one member that is inherited
from its only super class and this is ptr_.
So the following is telling the compiler to treat the value of our Local,
*global, as a pointer (which it already is) to a pointer that points to
a memory location that adhers to the layout of an v8::internal::Object type,
which we know now has a prt_ member. And we want to dereference it and pass
it into the function.
_v8_internal_Print_Object(*((v8::internal::Object**)(*global)));
ObjectTemplate
But I'm still missing the connection between ObjectTemplate and object. When we create it we use:
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
In src/api/api.cc we have:
static Local<ObjectTemplate> ObjectTemplateNew(
i::Isolate* isolate, v8::Local<FunctionTemplate> constructor,
bool do_not_cache) {
i::Handle<i::Struct> struct_obj = isolate->factory()->NewStruct(
i::OBJECT_TEMPLATE_INFO_TYPE, i::AllocationType::kOld);
i::Handle<i::ObjectTemplateInfo> obj = i::Handle<i::ObjectTemplateInfo>::cast(struct_obj);
InitializeTemplate(obj, Consts::OBJECT_TEMPLATE);
int next_serial_number = 0;
if (!constructor.IsEmpty())
obj->set_constructor(*Utils::OpenHandle(*constructor));
obj->set_data(i::Smi::zero());
return Utils::ToLocal(obj);
}
What is a Struct in this context?
src/objects/struct.h
#include "torque-generated/class-definitions-tq.h"
class Struct : public TorqueGeneratedStruct<Struct, HeapObject> {
public:
inline void InitializeBody(int object_size);
void BriefPrintDetails(std::ostream& os);
TQ_OBJECT_CONSTRUCTORS(Struct)
Notice that the include is specifying torque-generated include which can be
found out/x64.release_gcc/gen/torque-generated/class-definitions-tq. So, somewhere
there must be an call to the torque executable which generates the Code Stub
Assembler C++ headers and sources before compiling the main source files. There is
and there is a section about this in Building V8.
The macro TQ_OBJECT_CONSTRUCTORS can be found in src/objects/object-macros.h
and expands to:
constexpr Struct() = default;
protected:
template <typename TFieldType, int kFieldOffset>
friend class TaggedField;
inline explicit Struct(Address ptr);
So what does the TorqueGeneratedStruct look like?
template <class D, class P>
class TorqueGeneratedStruct : public P {
public:
Where D is Struct and P is HeapObject in this case. But the above is the declartion of the type but what we have in the .h file is what was generated.
This type is defined in src/objects/struct.tq:
@abstract
@generatePrint
@generateCppClass
extern class Struct extends HeapObject {
}
NewStruct can be found in src/heap/factory-base.cc
template <typename Impl>
HandleFor<Impl, Struct> FactoryBase<Impl>::NewStruct(
InstanceType type, AllocationType allocation) {
Map map = Map::GetStructMap(read_only_roots(), type);
int size = map.instance_size();
HeapObject result = AllocateRawWithImmortalMap(size, allocation, map);
HandleFor<Impl, Struct> str = handle(Struct::cast(result), isolate());
str->InitializeBody(size);
return str;
}
Every object that is stored on the v8 heap has a Map (src/objects/map.h) that
describes the structure of the object being stored.
class Map : public HeapObject {
1725 return Utils::ToLocal(obj);
(gdb) p obj
\$6 = {<v8::internal::HandleBase> = {location_ = 0x30b5160}, <No data fields>}
So this is the connection, what we see as a Local
(lldb) expr gl
(v8::internal::Object **) \$0 = 0x00000000020ee160
(lldb) memory read -f x -s 8 -c 1 gl
0x020ee160: 0x00000aee081c0121
(lldb) memory read -f x -s 8 -c 1 *gl
0xaee081c0121: 0x0200000002080433
You can reload .lldbinit using the following command:
(lldb) command source ~/.lldbinit
This can be useful when debugging a lldb command. You can set a breakpoint and break at that location and make updates to the command and reload without having to restart lldb.
Currently, the lldb-commands.py that ships with v8 contains an extra operation
of the parameter pased to ptr_arg_cmd:
def ptr_arg_cmd(debugger, name, param, cmd):
if not param:
print("'{}' requires an argument".format(name))
return
param = '(void*)({})'.format(param)
no_arg_cmd(debugger, cmd.format(param))
Notice that param is the object that we want to print, for example lets say
it is a local named obj:
param = "(void*)(obj)"
This will then be "passed"/formatted into the command string:
"_v8_internal_Print_Object(*(v8::internal::Object**)(*(void*)(obj))")
Threads
V8 is single threaded (the execution of the functions of the stack) but there are supporting threads used for garbage collection, profiling (IC, and perhaps other things) (I think). Lets see what threads there are:
$ LD_LIBRARY_PATH=../v8_src/v8/out/x64.release_gcc/ lldb ./hello-world
(lldb) br s -n main
(lldb) r
(lldb) thread list
thread #1: tid = 0x2efca6, 0x0000000100001e16 hello-world`main(argc=1, argv=0x00007fff5fbfee98) + 38 at hello-world.cc:40, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
So at startup there is only one thread which is what we expected. Lets skip ahead to where we create the platform:
Platform* platform = platform::CreateDefaultPlatform();
...
DefaultPlatform* platform = new DefaultPlatform(idle_task_support, tracing_controller);
platform->SetThreadPoolSize(thread_pool_size);
(lldb) fr v thread_pool_size
(int) thread_pool_size = 0
Next there is a check for 0 and the number of processors -1 is used as the size of the thread pool:
(lldb) fr v thread_pool_size
(int) thread_pool_size = 7
This is all that SetThreadPoolSize does. After this we have:
platform->EnsureInitialized();
for (int i = 0; i < thread_pool_size_; ++i)
thread_pool_.push_back(new WorkerThread(&queue_));
new WorkerThread will create a new pthread (on my system which is MacOSX):
result = pthread_create(&data_->thread_, &attr, ThreadEntry, this);
ThreadEntry can be found in src/base/platform/platform-posix.
International Component for Unicode (ICU)
International Components for Unicode (ICU) deals with internationalization (i18n). ICU provides support locale-sensitve string comparisons, date/time/number/currency formatting etc.
There is an optional API called ECMAScript 402 which V8 suppports and which is enabled by default. i18n-support says that even if your application does not use ICU you still need to call InitializeICU :
V8::InitializeICU();
Local
Local<String> script_name = ...;
So what is script_name. Well it is an object reference that is managed by the v8 GC. The GC needs to be able to move things (pointers around) and also track if things should be GC'd. Local handles as opposed to persistent handles are light weight and mostly used local operations. These handles are managed by HandleScopes so you must have a handlescope on the stack and the local is only valid as long as the handlescope is valid. This uses Resource Acquisition Is Initialization (RAII) so when the HandleScope instance goes out of scope it will remove all the Local instances.
The Local class (in include/v8.h) only has one member which is of type
pointer to the type T. So for the above example it would be:
String* val_;
You can find the available operations for a Local in include/v8.h.
(lldb) p script_name.IsEmpty()
(bool) \$12 = false
A Local
(lldb) p script_name->Length()
(int) \$14 = 7
Where Length is a method on the v8 String class.
The handle stack is not part of the C++ call stack, but the handle scopes are embedded in the C++ stack. Handle scopes can only be stack-allocated, not allocated with new.
Persistent
https://v8.dev/docs/embed: Persistent handles provide a reference to a heap-allocated JavaScript Object, just like a local handle. There are two flavors, which differ in the lifetime management of the reference they handle. Use a persistent handle when you need to keep a reference to an object for more than one function call, or when handle lifetimes do not correspond to C++ scopes. Google Chrome, for example, uses persistent handles to refer to Document Object Model (DOM) nodes.
A persistent handle can be made weak, using PersistentBase::SetWeak, to trigger a callback from the garbage collector when the only references to an object are from weak persistent handles.
A UniquePersistent
So how is a persistent object created?
Let's write a test and find out (test/persistent-object_text.cc):
$ make test/persistent-object_test
$ ./test/persistent-object_test --gtest_filter=PersistentTest.value
Now, to create an instance of Persistent we need a Local
Local<Object> o = Local<Object>::New(isolate_, Object::New(isolate_));
Local<Object>::New can be found in src/api/api.cc:
Local<v8::Object> v8::Object::New(Isolate* isolate) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
LOG_API(i_isolate, Object, New);
ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate);
i::Handle<i::JSObject> obj =
i_isolate->factory()->NewJSObject(i_isolate->object_function());
return Utils::ToLocal(obj);
}
The first thing that happens is that the public Isolate pointer is cast to an
pointer to the internal Isolate type.
LOG_API is a macro in the same source file (src/api/api.cc):
#define LOG_API(isolate, class_name, function_name) \
i::RuntimeCallTimerScope _runtime_timer( \
isolate, i::RuntimeCallCounterId::kAPI_##class_name##_##function_name); \
LOG(isolate, ApiEntryCall("v8::" #class_name "::" #function_name))
If our case the preprocessor would expand that to:
i::RuntimeCallTimerScope _runtime_timer(
isolate, i::RuntimeCallCounterId::kAPI_Object_New);
LOG(isolate, ApiEntryCall("v8::Object::New))
LOG is a macro that can be found in src/log.h:
#define LOG(isolate, Call) \
do { \
v8::internal::Logger* logger = (isolate)->logger(); \
if (logger->is_logging()) logger->Call; \
} while (false)
And this would expand to:
v8::internal::Logger* logger = isolate->logger();
if (logger->is_logging()) logger->ApiEntryCall("v8::Object::New");
So with the LOG_API macro expanded we have:
Local<v8::Object> v8::Object::New(Isolate* isolate) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
i::RuntimeCallTimerScope _runtime_timer( isolate, i::RuntimeCallCounterId::kAPI_Object_New);
v8::internal::Logger* logger = isolate->logger();
if (logger->is_logging()) logger->ApiEntryCall("v8::Object::New");
ENTER_V8_NO_SCRIPT_NO_EXCEPTION(i_isolate);
i::Handle<i::JSObject> obj =
i_isolate->factory()->NewJSObject(i_isolate->object_function());
return Utils::ToLocal(obj);
}
Next we have ENTER_V8_NO_SCRIPT_NO_EXCEPTION:
#define ENTER_V8_NO_SCRIPT_NO_EXCEPTION(isolate) \
i::VMState<v8::OTHER> __state__((isolate)); \
i::DisallowJavascriptExecutionDebugOnly __no_script__((isolate)); \
i::DisallowExceptions __no_exceptions__((isolate))
So with the macros expanded we have:
Local<v8::Object> v8::Object::New(Isolate* isolate) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
i::RuntimeCallTimerScope _runtime_timer( isolate, i::RuntimeCallCounterId::kAPI_Object_New);
v8::internal::Logger* logger = isolate->logger();
if (logger->is_logging()) logger->ApiEntryCall("v8::Object::New");
i::VMState<v8::OTHER> __state__(i_isolate));
i::DisallowJavascriptExecutionDebugOnly __no_script__(i_isolate);
i::DisallowExceptions __no_exceptions__(i_isolate));
i::Handle<i::JSObject> obj =
i_isolate->factory()->NewJSObject(i_isolate->object_function());
return Utils::ToLocal(obj);
}
TODO: Look closer at VMState.
First, i_isolate->object_function() is called and the result passed to
NewJSObject. object_function is generated by a macro named
NATIVE_CONTEXT_FIELDS:
#define NATIVE_CONTEXT_FIELD_ACCESSOR(index, type, name) \
Handle<type> Isolate::name() { \
return Handle<type>(raw_native_context()->name(), this); \
} \
bool Isolate::is_##name(type* value) { \
return raw_native_context()->is_##name(value); \
}
NATIVE_CONTEXT_FIELDS(NATIVE_CONTEXT_FIELD_ACCESSOR)
NATIVE_CONTEXT_FIELDS is a macro in src/contexts and it c
#define NATIVE_CONTEXT_FIELDS(V) \
... \
V(OBJECT_FUNCTION_INDEX, JSFunction, object_function) \
Handle<type> Isolate::object_function() {
return Handle<JSFunction>(raw_native_context()->object_function(), this);
}
bool Isolate::is_object_function(JSFunction* value) {
return raw_native_context()->is_object_function(value);
}
I'm not clear on the different types of context, there is a native context, a "normal/public" context.
In src/contexts-inl.h we have the native_context function:
Context* Context::native_context() const {
Object* result = get(NATIVE_CONTEXT_INDEX);
DCHECK(IsBootstrappingOrNativeContext(this->GetIsolate(), result));
return reinterpret_cast<Context*>(result);
}
Context extends FixedArray so the get function is the get function of FixedArray and NATIVE_CONTEXT_INDEX
is the index into the array where the native context is stored.
Now, lets take a closer look at NewJSObject. If you search for NewJSObject in src/heap/factory.cc:
Handle<JSObject> Factory::NewJSObject(Handle<JSFunction> constructor, PretenureFlag pretenure) {
JSFunction::EnsureHasInitialMap(constructor);
Handle<Map> map(constructor->initial_map(), isolate());
return NewJSObjectFromMap(map, pretenure);
}
NewJSObjectFromMap
...
HeapObject* obj = AllocateRawWithAllocationSite(map, pretenure, allocation_site);
So we have created a new map
Map
So an HeapObject contains a pointer to a Map, or rather has a function that returns a pointer to Map. I can't see any member map in the HeapObject class.
Lets take a look at when a map is created.
(lldb) br s -f map_test.cc -l 63
Handle<Map> Factory::NewMap(InstanceType type,
int instance_size,
ElementsKind elements_kind,
int inobject_properties) {
HeapObject* result = isolate()->heap()->AllocateRawWithRetryOrFail(Map::kSize, MAP_SPACE);
result->set_map_after_allocation(*meta_map(), SKIP_WRITE_BARRIER);
return handle(InitializeMap(Map::cast(result), type, instance_size,
elements_kind, inobject_properties),
isolate());
}
We can see that the above is calling AllocateRawWithRetryOrFail on the heap
instance passing a size of 88 and specifying the MAP_SPACE:
HeapObject* Heap::AllocateRawWithRetryOrFail(int size, AllocationSpace space,
AllocationAlignment alignment) {
AllocationResult alloc;
HeapObject* result = AllocateRawWithLigthRetry(size, space, alignment);
if (result) return result;
isolate()->counters()->gc_last_resort_from_handles()->Increment();
CollectAllAvailableGarbage(GarbageCollectionReason::kLastResort);
{
AlwaysAllocateScope scope(isolate());
alloc = AllocateRaw(size, space, alignment);
}
if (alloc.To(&result)) {
DCHECK(result != exception());
return result;
}
// TODO(1181417): Fix this.
FatalProcessOutOfMemory("CALL_AND_RETRY_LAST");
return nullptr;
}
The default value for alignment is kWordAligned. Reading the docs in the header it says that this function
will try to perform an allocation of size 88 in the MAP_SPACE and if it fails a full GC will be performed
and the allocation retried.
Lets take a look at AllocateRawWithLigthRetry:
AllocationResult alloc = AllocateRaw(size, space, alignment);
AllocateRaw can be found in src/heap/heap-inl.h. There are different paths that will be taken depending on the
space parameteter. Since it is MAP_SPACE in our case we will focus on that path:
AllocationResult Heap::AllocateRaw(int size_in_bytes, AllocationSpace space, AllocationAlignment alignment) {
...
HeapObject* object = nullptr;
AllocationResult allocation;
if (OLD_SPACE == space) {
...
} else if (MAP_SPACE == space) {
allocation = map_space_->AllocateRawUnaligned(size_in_bytes);
}
...
}
map_space_ is a private member of Heap (src/heap/heap.h):
MapSpace* map_space_;
AllocateRawUnaligned can be found in src/heap/spaces-inl.h:
AllocationResult PagedSpace::AllocateRawUnaligned( int size_in_bytes, UpdateSkipList update_skip_list) {
if (!EnsureLinearAllocationArea(size_in_bytes)) {
return AllocationResult::Retry(identity());
}
HeapObject* object = AllocateLinearly(size_in_bytes);
MSAN_ALLOCATED_UNINITIALIZED_MEMORY(object->address(), size_in_bytes);
return object;
}
The default value for update_skip_list is UPDATE_SKIP_LIST.
So lets take a look at AllocateLinearly:
HeapObject* PagedSpace::AllocateLinearly(int size_in_bytes) {
Address current_top = allocation_info_.top();
Address new_top = current_top + size_in_bytes;
allocation_info_.set_top(new_top);
return HeapObject::FromAddress(current_top);
}
Recall that size_in_bytes in our case is 88.
(lldb) expr current_top
(v8::internal::Address) \$5 = 24847457492680
(lldb) expr new_top
(v8::internal::Address) \$6 = 24847457492768
(lldb) expr new_top - current_top
(unsigned long) \$7 = 88
Notice that first the top is set to the new_top and then the current_top is returned and that will be a pointer to the start of the object in memory (which in this case is of v8::internal::Map which is also of type HeapObject). I've been wondering why Map (and other HeapObject) don't have any member fields and only/mostly getters/setters for the various fields that make up an object. Well the answer is that pointers to instances of for example Map point to the first memory location of the instance. And the getters/setter functions use indexed to read/write to memory locations. The indexes are mostly in the form of enum fields that define the memory layout of the type.
Next, in AllocateRawUnaligned we have the MSAN_ALLOCATED_UNINITIALIZED_MEMORY macro:
MSAN_ALLOCATED_UNINITIALIZED_MEMORY(object->address(), size_in_bytes);
MSAN_ALLOCATED_UNINITIALIZED_MEMORY can be found in src/msan.h and ms stands for Memory Sanitizer and
would only be used if V8_US_MEMORY_SANITIZER is defined.
The returned object will be used to construct an AllocationResult when returned.
Back in AllocateRaw we have:
if (allocation.To(&object)) {
...
OnAllocationEvent(object, size_in_bytes);
}
return allocation;
This will return us in AllocateRawWithLightRetry:
AllocationResult alloc = AllocateRaw(size, space, alignment);
if (alloc.To(&result)) {
DCHECK(result != exception());
return result;
}
This will return us back in AllocateRawWithRetryOrFail:
HeapObject* result = AllocateRawWithLigthRetry(size, space, alignment);
if (result) return result;
And that return will return to NewMap in src/heap/factory.cc:
result->set_map_after_allocation(*meta_map(), SKIP_WRITE_BARRIER);
return handle(InitializeMap(Map::cast(result), type, instance_size,
elements_kind, inobject_properties),
isolate());
InitializeMap:
map->set_instance_type(type);
map->set_prototype(*null_value(), SKIP_WRITE_BARRIER);
map->set_constructor_or_backpointer(*null_value(), SKIP_WRITE_BARRIER);
map->set_instance_size(instance_size);
if (map->IsJSObjectMap()) {
DCHECK(!isolate()->heap()->InReadOnlySpace(map));
map->SetInObjectPropertiesStartInWords(instance_size / kPointerSize - inobject_properties);
DCHECK_EQ(map->GetInObjectProperties(), inobject_properties);
map->set_prototype_validity_cell(*invalid_prototype_validity_cell());
} else {
DCHECK_EQ(inobject_properties, 0);
map->set_inobject_properties_start_or_constructor_function_index(0);
map->set_prototype_validity_cell(Smi::FromInt(Map::kPrototypeChainValid));
}
map->set_dependent_code(DependentCode::cast(*empty_fixed_array()), SKIP_WRITE_BARRIER);
map->set_weak_cell_cache(Smi::kZero);
map->set_raw_transitions(MaybeObject::FromSmi(Smi::kZero));
map->SetInObjectUnusedPropertyFields(inobject_properties);
map->set_instance_descriptors(*empty_descriptor_array());
map->set_visitor_id(Map::GetVisitorId(map));
map->set_bit_field(0);
map->set_bit_field2(Map::IsExtensibleBit::kMask);
int bit_field3 = Map::EnumLengthBits::encode(kInvalidEnumCacheSentinel) |
Map::OwnsDescriptorsBit::encode(true) |
Map::ConstructionCounterBits::encode(Map::kNoSlackTracking);
map->set_bit_field3(bit_field3);
map->set_elements_kind(elements_kind); //HOLEY_ELEMENTS
map->set_new_target_is_base(true);
isolate()->counters()->maps_created()->Increment();
if (FLAG_trace_maps) LOG(isolate(), MapCreate(map));
return map;
Creating a new map (map_test.cc:
i::Handle<i::Map> map = i::Map::Create(asInternal(isolate_), 10);
std::cout << map->instance_type() << '\n';
Map::Create can be found in objects.cc:
Handle<Map> Map::Create(Isolate* isolate, int inobject_properties) {
Handle<Map> copy = Copy(handle(isolate->object_function()->initial_map()), "MapCreate");
So, the first thing that will happen is isolate->object_function() will be called. This is function
that is generated by the preprocessor.
// from src/context.h
#define NATIVE_CONTEXT_FIELDS(V) \
... \
V(OBJECT_FUNCTION_INDEX, JSFunction, object_function) \
// from src/isolate.h
#define NATIVE_CONTEXT_FIELD_ACCESSOR(index, type, name) \
Handle<type> Isolate::name() { \
return Handle<type>(raw_native_context()->name(), this); \
} \
bool Isolate::is_##name(type* value) { \
return raw_native_context()->is_##name(value); \
}
NATIVE_CONTEXT_FIELDS(NATIVE_CONTEXT_FIELD_ACCESSOR)
object_function() will become:
Handle<JSFunction> Isolate::object_function() {
return Handle<JSFunction>(raw_native_context()->object_function(), this);
}
Lets look closer at JSFunction::initial_map() in in object-inl.h:
Map* JSFunction::initial_map() {
return Map::cast(prototype_or_initial_map());
}
prototype_or_initial_map is generated by a macro:
ACCESSORS_CHECKED(JSFunction, prototype_or_initial_map, Object,
kPrototypeOrInitialMapOffset, map()->has_prototype_slot())
ACCESSORS_CHECKED can be found in src/objects/object-macros.h:
#define ACCESSORS_CHECKED(holder, name, type, offset, condition) \
ACCESSORS_CHECKED2(holder, name, type, offset, condition, condition)
#define ACCESSORS_CHECKED2(holder, name, type, offset, get_condition, \
set_condition) \
type* holder::name() const { \
type* value = type::cast(READ_FIELD(this, offset)); \
DCHECK(get_condition); \
return value; \
} \
void holder::set_##name(type* value, WriteBarrierMode mode) { \
DCHECK(set_condition); \
WRITE_FIELD(this, offset, value); \
CONDITIONAL_WRITE_BARRIER(GetHeap(), this, offset, value, mode); \
}
#define FIELD_ADDR(p, offset) \
(reinterpret_cast<Address>(p) + offset - kHeapObjectTag)
#define READ_FIELD(p, offset) \
(*reinterpret_cast<Object* const*>(FIELD_ADDR(p, offset)))
The preprocessor will expand prototype_or_initial_map to:
JSFunction* JSFunction::prototype_or_initial_map() const {
JSFunction* value = JSFunction::cast(
(*reinterpret_cast<Object* const*>(
(reinterpret_cast<Address>(this) + kPrototypeOrInitialMapOffset - kHeapObjectTag))))
DCHECK(map()->has_prototype_slot());
return value;
}
Notice that map()->has_prototype_slot()) will be called first which looks like this:
Map* HeapObject::map() const {
return map_word().ToMap();
}
TODO: Add notes about MapWord
MapWord HeapObject::map_word() const {
return MapWord(
reinterpret_cast<uintptr_t>(RELAXED_READ_FIELD(this, kMapOffset)));
}
First thing that will happen is RELAXED_READ_FIELD(this, kMapOffset)
#define RELAXED_READ_FIELD(p, offset) \
reinterpret_cast<Object*>(base::Relaxed_Load( \
reinterpret_cast<const base::AtomicWord*>(FIELD_ADDR(p, offset))))
#define FIELD_ADDR(p, offset) \
(reinterpret_cast<Address>(p) + offset - kHeapObjectTag)
This will get expanded by the preprocessor to:
reinterpret_cast<Object*>(base::Relaxed_Load(
reinterpret_cast<const base::AtomicWord*>(
(reinterpret_cast<Address>(this) + kMapOffset - kHeapObjectTag)))
src/base/atomicops_internals_portable.h:
inline Atomic8 Relaxed_Load(volatile const Atomic8* ptr) {
return __atomic_load_n(ptr, __ATOMIC_RELAXED);
}
So this will do an atomoic load of the ptr with the memory order of __ATOMIC_RELELAXED.
ACCESSORS_CHECKED also generates a set_prototyp_or_initial_map:
void JSFunction::set_prototype_or_initial_map(JSFunction* value, WriteBarrierMode mode) {
DCHECK(map()->has_prototype_slot());
WRITE_FIELD(this, kPrototypeOrInitialMapOffset, value);
CONDITIONAL_WRITE_BARRIER(GetHeap(), this, kPrototypeOrInitialMapOffset, value, mode);
}
What does WRITE_FIELD do?
#define WRITE_FIELD(p, offset, value) \
base::Relaxed_Store( \
reinterpret_cast<base::AtomicWord*>(FIELD_ADDR(p, offset)), \
reinterpret_cast<base::AtomicWord>(value));
Which would expand into:
base::Relaxed_Store( \
reinterpret_cast<base::AtomicWord*>(
(reinterpret_cast<Address>(this) + kPrototypeOrInitialMapOffset - kHeapObjectTag)
reinterpret_cast<base::AtomicWord>(value));
Lets take a look at what instance_type does:
InstanceType Map::instance_type() const {
return static_cast<InstanceType>(READ_UINT16_FIELD(this, kInstanceTypeOffset));
}
To see what the above is doing we can do the same thing in the debugger:
Note that I got 11 below from map->kInstanceTypeOffset - i::kHeapObjectTag
(lldb) memory read -f u -c 1 -s 8 `*map + 11`
0x6d4e6609ed4: 585472345729139745
(lldb) expr static_cast<InstanceType>(585472345729139745)
(v8::internal::InstanceType) \$34 = JS_OBJECT_TYPE
Take map->has_non_instance_prototype():
(lldb) br s -n has_non_instance_prototype
(lldb) expr -i 0 -- map->has_non_instance_prototype()
The above command will break in src/objects/map-inl.h:
BIT_FIELD_ACCESSORS(Map, bit_field, has_non_instance_prototype, Map::HasNonInstancePrototypeBit)
// src/objects/object-macros.h
#define BIT_FIELD_ACCESSORS(holder, field, name, BitField) \
typename BitField::FieldType holder::name() const { \
return BitField::decode(field()); \
} \
void holder::set_##name(typename BitField::FieldType value) { \
set_##field(BitField::update(field(), value)); \
}
The preprocessor will expand that to:
typename Map::HasNonInstancePrototypeBit::FieldType Map::has_non_instance_prototype() const {
return Map::HasNonInstancePrototypeBit::decode(bit_field());
} \
void holder::set_has_non_instance_prototype(typename BitField::FieldType value) { \
set_bit_field(Map::HasNonInstancePrototypeBit::update(bit_field(), value)); \
}
So where can we find Map::HasNonInstancePrototypeBit?
It is generated by a macro in src/objects/map.h:
// Bit positions for |bit_field|.
#define MAP_BIT_FIELD_FIELDS(V, _) \
V(HasNonInstancePrototypeBit, bool, 1, _) \
...
DEFINE_BIT_FIELDS(MAP_BIT_FIELD_FIELDS)
#undef MAP_BIT_FIELD_FIELDS
#define DEFINE_BIT_FIELDS(LIST_MACRO) \
DEFINE_BIT_RANGES(LIST_MACRO) \
LIST_MACRO(DEFINE_BIT_FIELD_TYPE, LIST_MACRO##_Ranges)
#define DEFINE_BIT_RANGES(LIST_MACRO) \
struct LIST_MACRO##_Ranges { \
enum { LIST_MACRO(DEFINE_BIT_FIELD_RANGE_TYPE, _) kBitsCount }; \
};
#define DEFINE_BIT_FIELD_RANGE_TYPE(Name, Type, Size, _) \
k##Name##Start, k##Name##End = k##Name##Start + Size - 1,
Alright, lets see what preprocessor expands that to:
struct MAP_BIT_FIELD_FIELDS_Ranges {
enum {
kHasNonInstancePrototypeBitStart,
kHasNonInstancePrototypeBitEnd = kHasNonInstancePrototypeBitStart + 1 - 1,
... // not showing the rest of the entries.
kBitsCount
};
};
So this would create a struct with an enum and it could be accessed using:
i::Map::MAP_BIT_FIELD_FIELDS_Ranges::kHasNonInstancePrototypeBitStart
The next part of the macro is
LIST_MACRO(DEFINE_BIT_FIELD_TYPE, LIST_MACRO##_Ranges)
#define DEFINE_BIT_FIELD_TYPE(Name, Type, Size, RangesName) \
typedef BitField<Type, RangesName::k##Name##Start, Size> Name;
Which will get expanded to:
typedef BitField<HasNonInstancePrototypeBit, MAP_BIT_FIELD_FIELDS_Ranges::kHasNonInstancePrototypeBitStart, 1> HasNonInstancePrototypeBit;
So this is how HasNonInstancePrototypeBit is declared and notice that it is of type BitField which can be
found in src/utils.h:
template<class T, int shift, int size>
class BitField : public BitFieldBase<T, shift, size, uint32_t> { };
template<class T, int shift, int size, class U>
class BitFieldBase {
public:
typedef T FieldType;
Map::HasNonInstancePrototypeBit::decode(bit_field()); first bit_field is called:
byte Map::bit_field() const { return READ_BYTE_FIELD(this, kBitFieldOffset); }
And the result of that is passed to Map::HasNonInstancePrototypeBit::decode:
(lldb) br s -n bit_field
(lldb) expr -i 0 -- map->bit_field()
byte Map::bit_field() const { return READ_BYTE_FIELD(this, kBitFieldOffset); }
So, this is the current Map instance, and we are going to read from.
#define READ_BYTE_FIELD(p, offset) \
(*reinterpret_cast<const byte*>(FIELD_ADDR(p, offset)))
#define FIELD_ADDR(p, offset) \
(reinterpret_cast<Address>(p) + offset - kHeapObjectTag)
Which will get expanded to:
byte Map::bit_field() const {
return *reinterpret_cast<const byte*>(
reinterpret_cast<Address>(this) + kBitFieldOffset - kHeapObjectTag)
}
The instance_size is the instance_size_in_words << kPointerSizeLog2 (3 on my machine):
(lldb) memory read -f x -s 1 -c 1 *map+8
0x24d1cd509ed1: 0x03
(lldb) expr 0x03 << 3
(int) \$2 = 24
(lldb) expr map->instance_size()
(int) \$3 = 24
i::HeapObject::kHeaderSize is 8 on my system which is used in the `DEFINE_FIELD_OFFSET_CONSTANTS:
#define MAP_FIELDS(V)
V(kInstanceSizeInWordsOffset, kUInt8Size)
V(kInObjectPropertiesStartOrConstructorFunctionIndexOffset, kUInt8Size)
...
DEFINE_FIELD_OFFSET_CONSTANTS(HeapObject::kHeaderSize, MAP_FIELDS)
So we can use this information to read the inobject_properties_start_or_constructor_function_index directly from memory using:
(lldb) expr map->inobject_properties_start_or_constructor_function_index()
(lldb) memory read -f x -s 1 -c 1 map+9
error: invalid start address expression.
error: address expression "map+9" evaluation failed
(lldb) memory read -f x -s 1 -c 1 *map+9
0x17b027209ed2: 0x03
Inspect the visitor_id (which is the last of the first byte):
lldb) memory read -f x -s 1 -c 1 *map+10
0x17b027209ed3: 0x15
(lldb) expr (int) 0x15
(int) \$8 = 21
(lldb) expr map->visitor_id()
(v8::internal::VisitorId) \$11 = kVisitJSObjectFast
(lldb) expr (int) \$11
(int) \$12 = 21
Inspect the instance_type (which is part of the second byte):
(lldb) expr map->instance_type()
(v8::internal::InstanceType) \$41 = JS_OBJECT_TYPE
(lldb) expr v8::internal::InstanceType::JS_OBJECT_TYPE
(uint16_t) \$35 = 1057
(lldb) memory read -f x -s 2 -c 1 *map+11
0x17b027209ed4: 0x0421
(lldb) expr (int)0x0421
(int) \$40 = 1057
Notice that instance_type is a short so that will take up 2 bytes
(lldb) expr map->has_non_instance_prototype()
(bool) \$60 = false
(lldb) expr map->is_callable()
(bool) \$46 = false
(lldb) expr map->has_named_interceptor()
(bool) \$51 = false
(lldb) expr map->has_indexed_interceptor()
(bool) \$55 = false
(lldb) expr map->is_undetectable()
(bool) \$56 = false
(lldb) expr map->is_access_check_needed()
(bool) \$57 = false
(lldb) expr map->is_constructor()
(bool) \$58 = false
(lldb) expr map->has_prototype_slot()
(bool) \$59 = false
Verify that the above is correct:
(lldb) expr map->has_non_instance_prototype()
(bool) \$44 = false
(lldb) memory read -f x -s 1 -c 1 *map+13
0x17b027209ed6: 0x00
(lldb) expr map->set_has_non_instance_prototype(true)
(lldb) memory read -f x -s 1 -c 1 *map+13
0x17b027209ed6: 0x01
(lldb) expr map->set_has_prototype_slot(true)
(lldb) memory read -f x -s 1 -c 1 *map+13
0x17b027209ed6: 0x81
Inspect second int field (bit_field2):
(lldb) memory read -f x -s 1 -c 1 *map+14
0x17b027209ed7: 0x19
(lldb) expr map->is_extensible()
(bool) \$78 = true
(lldb) expr -- 0x19 & (1 << 0)
(bool) \$90 = 1
(lldb) expr map->is_prototype_map()
(bool) \$79 = false
(lldb) expr map->is_in_retained_map_list()
(bool) \$80 = false
(lldb) expr map->elements_kind()
(v8::internal::ElementsKind) \$81 = HOLEY_ELEMENTS
(lldb) expr v8::internal::ElementsKind::HOLEY_ELEMENTS
(int) \$133 = 3
(lldb) expr 0x19 >> 3
(int) \$134 = 3
Inspect third int field (bit_field3):
(lldb) memory read -f b -s 4 -c 1 *map+15
0x17b027209ed8: 0b00001000001000000000001111111111
(lldb) memory read -f x -s 4 -c 1 *map+15
0x17b027209ed8: 0x082003ff
So we know that a Map instance is a pointer allocated by the Heap and with a specific size. Fields are accessed using indexes (remember there are no member fields in the Map class). We also know that all HeapObject have a Map. The Map is sometimes referred to as the HiddenClass and sometimes the shape of an object. If two objects have the same properties they would share the same Map. This makes sense and I've see blog post that show this but I'd like to verify this to fully understand it. I'm going to try to match https://v8project.blogspot.com/2017/08/fast-properties.html with the code.
So, lets take a look at adding a property to a JSObject. We start by creating a new Map and then use it to create a new JSObject:
i::Handle<i::Map> map = factory->NewMap(i::JS_OBJECT_TYPE, 32);
i::Handle<i::JSObject> js_object = factory->NewJSObjectFromMap(map);
i::Handle<i::String> prop_name = factory->InternalizeUtf8String("prop_name");
i::Handle<i::String> prop_value = factory->InternalizeUtf8String("prop_value");
i::JSObject::AddProperty(js_object, prop_name, prop_value, i::NONE);
Lets take a closer look at AddProperty and how it interacts with the Map. This function can be
found in src/objects.cc:
void JSObject::AddProperty(Handle<JSObject> object, Handle<Name> name,
Handle<Object> value,
PropertyAttributes attributes) {
LookupIterator it(object, name, object, LookupIterator::OWN_SKIP_INTERCEPTOR);
CHECK_NE(LookupIterator::ACCESS_CHECK, it.state());
First we have the LookupIterator constructor (src/lookup.h) but since this is a new property which
we know does not exist it will not find any property.
CHECK(AddDataProperty(&it, value, attributes, kThrowOnError,
CERTAINLY_NOT_STORE_FROM_KEYED)
.IsJust());
Handle<JSReceiver> receiver = it->GetStoreTarget<JSReceiver>();
...
it->UpdateProtector();
// Migrate to the most up-to-date map that will be able to store |value|
// under it->name() with |attributes|.
it->PrepareTransitionToDataProperty(receiver, value, attributes, store_mode);
DCHECK_EQ(LookupIterator::TRANSITION, it->state());
it->ApplyTransitionToDataProperty(receiver);
// Write the property value.
it->WriteDataValue(value, true);
PrepareTransitionToDataProperty:
Representation representation = value->OptimalRepresentation();
Handle<FieldType> type = value->OptimalType(isolate, representation);
maybe_map = Map::CopyWithField(map, name, type, attributes, constness,
representation, flag);
Map::CopyWithField:
Descriptor d = Descriptor::DataField(name, index, attributes, constness, representation, wrapped_type);
Lets take a closer look the Decriptor which can be found in src/property.cc:
Descriptor Descriptor::DataField(Handle<Name> key, int field_index,
PropertyAttributes attributes,
PropertyConstness constness,
Representation representation,
MaybeObjectHandle wrapped_field_type) {
DCHECK(wrapped_field_type->IsSmi() || wrapped_field_type->IsWeakHeapObject());
PropertyDetails details(kData, attributes, kField, constness, representation,
field_index);
return Descriptor(key, wrapped_field_type, details);
}
Descriptor is declared in src/property.h and describes the elements in a instance-descriptor array. These
are returned when calling map->instance_descriptors(). Let check some of the arguments:
(lldb) job *key
#prop_name
(lldb) expr attributes
(v8::internal::PropertyAttributes) \$27 = NONE
(lldb) expr constness
(v8::internal::PropertyConstness) \$28 = kMutable
(lldb) expr representation
(v8::internal::Representation) \$29 = (kind_ = '\b')
The Descriptor class contains three members:
private:
Handle<Name> key_;
MaybeObjectHandle value_;
PropertyDetails details_;
Lets take a closer look PropertyDetails which only has a single member named value_
uint32_t value_;
It also declares a number of classes the extend BitField, for example:
class KindField : public BitField<PropertyKind, 0, 1> {};
class LocationField : public BitField<PropertyLocation, KindField::kNext, 1> {};
class ConstnessField : public BitField<PropertyConstness, LocationField::kNext, 1> {};
class AttributesField : public BitField<PropertyAttributes, ConstnessField::kNext, 3> {};
class PropertyCellTypeField : public BitField<PropertyCellType, AttributesField::kNext, 2> {};
class DictionaryStorageField : public BitField<uint32_t, PropertyCellTypeField::kNext, 23> {};
// Bit fields for fast objects.
class RepresentationField : public BitField<uint32_t, AttributesField::kNext, 4> {};
class DescriptorPointer : public BitField<uint32_t, RepresentationField::kNext, kDescriptorIndexBitCount> {};
class FieldIndexField : public BitField<uint32_t, DescriptorPointer::kNext, kDescriptorIndexBitCount> {
enum PropertyKind { kData = 0, kAccessor = 1 };
enum PropertyLocation { kField = 0, kDescriptor = 1 };
enum class PropertyConstness { kMutable = 0, kConst = 1 };
enum PropertyAttributes {
NONE = ::v8::None,
READ_ONLY = ::v8::ReadOnly,
DONT_ENUM = ::v8::DontEnum,
DONT_DELETE = ::v8::DontDelete,
ALL_ATTRIBUTES_MASK = READ_ONLY | DONT_ENUM | DONT_DELETE,
SEALED = DONT_DELETE,
FROZEN = SEALED | READ_ONLY,
ABSENT = 64, // Used in runtime to indicate a property is absent.
// ABSENT can never be stored in or returned from a descriptor's attributes
// bitfield. It is only used as a return value meaning the attributes of
// a non-existent property.
};
enum class PropertyCellType {
// Meaningful when a property cell does not contain the hole.
kUndefined, // The PREMONOMORPHIC of property cells.
kConstant, // Cell has been assigned only once.
kConstantType, // Cell has been assigned only one type.
kMutable, // Cell will no longer be tracked as constant.
// Meaningful when a property cell contains the hole.
kUninitialized = kUndefined, // Cell has never been initialized.
kInvalidated = kConstant, // Cell has been deleted, invalidated or never
// existed.
// For dictionaries not holding cells.
kNoCell = kMutable,
};
template<class T, int shift, int size>
class BitField : public BitFieldBase<T, shift, size, uint32_t> { };
The Type T of KindField will be PropertyKind, the shift will be 0 , and the size 1.
Notice that LocationField is using KindField::kNext as its shift. This is a static class constant
of type uint32_t and is defined as:
static const U kNext = kShift + kSize;
So LocationField would get the value from KindField which should be:
class LocationField : public BitField<PropertyLocation, 1, 1> {};
The constructor for PropertyDetails looks like this:
PropertyDetails(PropertyKind kind, PropertyAttributes attributes, PropertyCellType cell_type, int dictionary_index = 0) {
value_ = KindField::encode(kind) | LocationField::encode(kField) |
AttributesField::encode(attributes) |
DictionaryStorageField::encode(dictionary_index) |
PropertyCellTypeField::encode(cell_type);
}
So what does KindField::encode(kind) actualy do then?
(lldb) expr static_cast<uint32_t>(kind())
(uint32_t) \$36 = 0
(lldb) expr static_cast<uint32_t>(kind()) << 0
(uint32_t) \$37 = 0
This value is later returned by calling kind():
PropertyKind kind() const { return KindField::decode(value_); }
So we have all this information about this property, its type (Representation), constness, if it is
read-only, enumerable, deletable, sealed, frozen. After that little detour we are back in Descriptor::DataField:
return Descriptor(key, wrapped_field_type, details);
Here we are using the key (name of the property), the wrapped_field_type, and PropertyDetails we created.
What is wrapped_field_type again?
If we back up a few frames back into Map::TransitionToDataProperty we can see that the type passed in
is taken from the following code:
Representation representation = value->OptimalRepresentation();
Handle<FieldType> type = value->OptimalType(isolate, representation);
So this is only taking the type of the field:
(lldb) expr representation.kind()
(v8::internal::Representation::Kind) \$51 = kHeapObject
This makes sense as the map only deals with the shape of the propery and not the value.
Next in Map::CopyWithField we have:
Handle<Map> new_map = Map::CopyAddDescriptor(map, &d, flag);
CopyAddDescriptor does:
Handle<DescriptorArray> descriptors(map->instance_descriptors());
int nof = map->NumberOfOwnDescriptors();
Handle<DescriptorArray> new_descriptors = DescriptorArray::CopyUpTo(descriptors, nof, 1);
new_descriptors->Append(descriptor);
Handle<LayoutDescriptor> new_layout_descriptor =
FLAG_unbox_double_fields
? LayoutDescriptor::New(map, new_descriptors, nof + 1)
: handle(LayoutDescriptor::FastPointerLayout(), map->GetIsolate());
return CopyReplaceDescriptors(map, new_descriptors, new_layout_descriptor,
flag, descriptor->GetKey(), "CopyAddDescriptor",
SIMPLE_PROPERTY_TRANSITION);
Lets take a closer look at LayoutDescriptor
(lldb) expr new_layout_descriptor->Print()
Layout descriptor: <all tagged>
TODO: Take a closer look at LayoutDescritpor
Later when actually adding the value in Object::AddDataProperty:
it->WriteDataValue(value, true);
This call will end up in src/lookup.cc and in our case the path will be the following call:
JSObject::cast(*holder)->WriteToField(descriptor_number(), property_details_, *value);
TODO: Take a closer look at LookupIterator.
WriteToField can be found in src/objects-inl.h:
FieldIndex index = FieldIndex::ForDescriptor(map(), descriptor);
FieldIndex::ForDescriptor can be found in src/field-index-inl.h:
inline FieldIndex FieldIndex::ForDescriptor(const Map* map, int descriptor_index) {
PropertyDetails details = map->instance_descriptors()->GetDetails(descriptor_index);
int field_index = details.field_index();
return ForPropertyIndex(map, field_index, details.representation());
}
Notice that this is calling instance_descriptors() on the passed-in map. This as we recall from earlier returns
and DescriptorArray (which is a type of WeakFixedArray). A Descriptor array
Our DecsriptorArray only has one entry:
(lldb) expr map->instance_descriptors()->number_of_descriptors()
(int) \$6 = 1
(lldb) expr map->instance_descriptors()->GetKey(0)->Print()
#prop_name
(lldb) expr map->instance_descriptors()->GetFieldIndex(0)
(int) \$11 = 0
We can also use Print on the DescriptorArray:
lldb) expr map->instance_descriptors()->Print()
[0]: #prop_name (data field 0:h, p: 0, attrs: [WEC]) @ Any
In our case we are accessing the PropertyDetails and then getting the field_index which I think tells us
where in the object the value for this property is stored.
The last call in ForDescriptor is `ForProperty:
inline FieldIndex FieldIndex::ForPropertyIndex(const Map* map,
int property_index,
Representation representation) {
int inobject_properties = map->GetInObjectProperties();
bool is_inobject = property_index < inobject_properties;
int first_inobject_offset;
int offset;
if (is_inobject) {
first_inobject_offset = map->GetInObjectPropertyOffset(0);
offset = map->GetInObjectPropertyOffset(property_index);
} else {
first_inobject_offset = FixedArray::kHeaderSize;
property_index -= inobject_properties;
offset = FixedArray::kHeaderSize + property_index * kPointerSize;
}
Encoding encoding = FieldEncoding(representation);
return FieldIndex(is_inobject, offset, encoding, inobject_properties,
first_inobject_offset);
}
I was expecting inobject_propertis to be 1 here but it is 0:
(lldb) expr inobject_properties
(int) \$14 = 0
Why is that, what am I missing?
These in-object properties are stored directly on the object instance and not do not use
the properties array. All get back to an example of this later to clarify this.
TODO: Add in-object properties example.
Back in JSObject::WriteToField:
RawFastPropertyAtPut(index, value);
void JSObject::RawFastPropertyAtPut(FieldIndex index, Object* value) {
if (index.is_inobject()) {
int offset = index.offset();
WRITE_FIELD(this, offset, value);
WRITE_BARRIER(GetHeap(), this, offset, value);
} else {
property_array()->set(index.outobject_array_index(), value);
}
}
In our case we know that the index is not inobject()
(lldb) expr index.is_inobject()
(bool) \$18 = false
So, property_array()->set() will be called.
(lldb) expr this
(v8::internal::JSObject *) \$21 = 0x00002c31c6a88b59
JSObject inherits from JSReceiver which is where the property_array() function is declared.
inline PropertyArray* property_array() const;
(lldb) expr property_array()->Print()
0x2c31c6a88bb1: [PropertyArray]
- map: 0x2c31f5603e21 <Map>
- length: 3
- hash: 0
0: 0x2c31f56025a1 <Odd Oddball: uninitialized>
1-2: 0x2c31f56026f1 <undefined>
(lldb) expr index.outobject_array_index()
(int) \$26 = 0
(lldb) expr value->Print()
#prop_value
Looking at the above values printed we should see the property be written to entry 0.
(lldb) expr property_array()->get(0)->Print()
#uninitialized
// after call to set
(lldb) expr property_array()->get(0)->Print()
#prop_value
(lldb) expr map->instance_descriptors()
(v8::internal::DescriptorArray *) \$4 = 0x000039a927082339
So a map has an pointer array of instance of DescriptorArray
(lldb) expr map->GetInObjectProperties()
(int) \$19 = 1
Each Map has int that tells us the number of properties it has. This is the number specified when creating a new Map, for example:
i::Handle<i::Map> map = i::Map::Create(asInternal(isolate_), 1);
But at this stage we don't really have any properties. The value for a property is associated with the actual instance of the Object. What the Map specifies is index of the value for a particualar property.
Creating a Map instance
Lets take a look at when a map is created.
(lldb) br s -f map_test.cc -l 63
Handle<Map> Factory::NewMap(InstanceType type,
int instance_size,
ElementsKind elements_kind,
int inobject_properties) {
HeapObject* result = isolate()->heap()->AllocateRawWithRetryOrFail(Map::kSize, MAP_SPACE);
result->set_map_after_allocation(*meta_map(), SKIP_WRITE_BARRIER);
return handle(InitializeMap(Map::cast(result), type, instance_size,
elements_kind, inobject_properties),
isolate());
}
We can see that the above is calling AllocateRawWithRetryOrFail on the heap instance passing a size of 88 and
specifying the MAP_SPACE:
HeapObject* Heap::AllocateRawWithRetryOrFail(int size, AllocationSpace space,
AllocationAlignment alignment) {
AllocationResult alloc;
HeapObject* result = AllocateRawWithLigthRetry(size, space, alignment);
if (result) return result;
isolate()->counters()->gc_last_resort_from_handles()->Increment();
CollectAllAvailableGarbage(GarbageCollectionReason::kLastResort);
{
AlwaysAllocateScope scope(isolate());
alloc = AllocateRaw(size, space, alignment);
}
if (alloc.To(&result)) {
DCHECK(result != exception());
return result;
}
// TODO(1181417): Fix this.
FatalProcessOutOfMemory("CALL_AND_RETRY_LAST");
return nullptr;
}
The default value for alignment is kWordAligned. Reading the docs in the header it says that this function
will try to perform an allocation of size 88 in the MAP_SPACE and if it fails a full GC will be performed
and the allocation retried.
Lets take a look at AllocateRawWithLigthRetry:
AllocationResult alloc = AllocateRaw(size, space, alignment);
AllocateRaw can be found in src/heap/heap-inl.h. There are different paths that will be taken depending on the
space parameteter. Since it is MAP_SPACE in our case we will focus on that path:
AllocationResult Heap::AllocateRaw(int size_in_bytes, AllocationSpace space, AllocationAlignment alignment) {
...
HeapObject* object = nullptr;
AllocationResult allocation;
if (OLD_SPACE == space) {
...
} else if (MAP_SPACE == space) {
allocation = map_space_->AllocateRawUnaligned(size_in_bytes);
}
...
}
map_space_ is a private member of Heap (src/heap/heap.h):
MapSpace* map_space_;
AllocateRawUnaligned can be found in src/heap/spaces-inl.h:
AllocationResult PagedSpace::AllocateRawUnaligned( int size_in_bytes, UpdateSkipList update_skip_list) {
if (!EnsureLinearAllocationArea(size_in_bytes)) {
return AllocationResult::Retry(identity());
}
HeapObject* object = AllocateLinearly(size_in_bytes);
MSAN_ALLOCATED_UNINITIALIZED_MEMORY(object->address(), size_in_bytes);
return object;
}
The default value for update_skip_list is UPDATE_SKIP_LIST.
So lets take a look at AllocateLinearly:
HeapObject* PagedSpace::AllocateLinearly(int size_in_bytes) {
Address current_top = allocation_info_.top();
Address new_top = current_top + size_in_bytes;
allocation_info_.set_top(new_top);
return HeapObject::FromAddress(current_top);
}
Recall that size_in_bytes in our case is 88.
(lldb) expr current_top
(v8::internal::Address) \$5 = 24847457492680
(lldb) expr new_top
(v8::internal::Address) \$6 = 24847457492768
(lldb) expr new_top - current_top
(unsigned long) \$7 = 88
Notice that first the top is set to the new_top and then the current_top is returned and that will be a pointer to the start of the object in memory (which in this case is of v8::internal::Map which is also of type HeapObject). I've been wondering why Map (and other HeapObject) don't have any member fields and only/mostly getters/setters for the various fields that make up an object. Well the answer is that pointers to instances of for example Map point to the first memory location of the instance. And the getters/setter functions use indexed to read/write to memory locations. The indexes are mostly in the form of enum fields that define the memory layout of the type.
Next, in AllocateRawUnaligned we have the MSAN_ALLOCATED_UNINITIALIZED_MEMORY macro:
MSAN_ALLOCATED_UNINITIALIZED_MEMORY(object->address(), size_in_bytes);
MSAN_ALLOCATED_UNINITIALIZED_MEMORY can be found in src/msan.h and ms stands for Memory Sanitizer and
would only be used if V8_US_MEMORY_SANITIZER is defined.
The returned object will be used to construct an AllocationResult when returned.
Back in AllocateRaw we have:
if (allocation.To(&object)) {
...
OnAllocationEvent(object, size_in_bytes);
}
return allocation;
This will return us in AllocateRawWithLightRetry:
AllocationResult alloc = AllocateRaw(size, space, alignment);
if (alloc.To(&result)) {
DCHECK(result != exception());
return result;
}
This will return us back in AllocateRawWithRetryOrFail:
HeapObject* result = AllocateRawWithLigthRetry(size, space, alignment);
if (result) return result;
And that return will return to NewMap in src/heap/factory.cc:
result->set_map_after_allocation(*meta_map(), SKIP_WRITE_BARRIER);
return handle(InitializeMap(Map::cast(result), type, instance_size,
elements_kind, inobject_properties),
isolate());
InitializeMap:
map->set_instance_type(type);
map->set_prototype(*null_value(), SKIP_WRITE_BARRIER);
map->set_constructor_or_backpointer(*null_value(), SKIP_WRITE_BARRIER);
map->set_instance_size(instance_size);
if (map->IsJSObjectMap()) {
DCHECK(!isolate()->heap()->InReadOnlySpace(map));
map->SetInObjectPropertiesStartInWords(instance_size / kPointerSize - inobject_properties);
DCHECK_EQ(map->GetInObjectProperties(), inobject_properties);
map->set_prototype_validity_cell(*invalid_prototype_validity_cell());
} else {
DCHECK_EQ(inobject_properties, 0);
map->set_inobject_properties_start_or_constructor_function_index(0);
map->set_prototype_validity_cell(Smi::FromInt(Map::kPrototypeChainValid));
}
map->set_dependent_code(DependentCode::cast(*empty_fixed_array()), SKIP_WRITE_BARRIER);
map->set_weak_cell_cache(Smi::kZero);
map->set_raw_transitions(MaybeObject::FromSmi(Smi::kZero));
map->SetInObjectUnusedPropertyFields(inobject_properties);
map->set_instance_descriptors(*empty_descriptor_array());
map->set_visitor_id(Map::GetVisitorId(map));
map->set_bit_field(0);
map->set_bit_field2(Map::IsExtensibleBit::kMask);
int bit_field3 = Map::EnumLengthBits::encode(kInvalidEnumCacheSentinel) |
Map::OwnsDescriptorsBit::encode(true) |
Map::ConstructionCounterBits::encode(Map::kNoSlackTracking);
map->set_bit_field3(bit_field3);
map->set_elements_kind(elements_kind); //HOLEY_ELEMENTS
map->set_new_target_is_base(true);
isolate()->counters()->maps_created()->Increment();
if (FLAG_trace_maps) LOG(isolate(), MapCreate(map));
return map;
Context
Context extends FixedArray (src/context.h). So an instance of this Context is a FixedArray and we can
use Get(index) etc to get entries in the array.
V8_EXPORT
This can be found in quite a few places in v8 source code. For example:
class V8_EXPORT ArrayBuffer : public Object {
What is this?
It is a preprocessor macro which looks like this:
#if V8_HAS_ATTRIBUTE_VISIBILITY && defined(V8_SHARED)
# ifdef BUILDING_V8_SHARED
# define V8_EXPORT __attribute__ ((visibility("default")))
# else
# define V8_EXPORT
# endif
#else
# define V8_EXPORT
#endif
So we can see that if V8_HAS_ATTRIBUTE_VISIBILITY, and defined(V8_SHARED), and also
if BUILDING_V8_SHARED, V8_EXPORT is set to __attribute__ ((visibility("default")).
But in all other cases V8_EXPORT is empty and the preprocessor does not insert
anything (nothing will be there come compile time).
But what about the __attribute__ ((visibility("default")) what is this?
In the GNU compiler collection (GCC) environment, the term that is used for exporting is visibility. As it applies to functions and variables in a shared object, visibility refers to the ability of other shared objects to call a C/C++ function. Functions with default visibility have a global scope and can be called from other shared objects. Functions with hidden visibility have a local scope and cannot be called from other shared objects.
Visibility can be controlled by using either compiler options or visibility attributes.
In your header files, wherever you want an interface or API made public outside
the current Dynamic Shared Object (DSO) , place
__attribute__ ((visibility ("default"))) in struct, class and function
declarations you wish to make public. With -fvisibility=hidden, you are
telling GCC that every declaration not explicitly marked with a visibility
attribute has a hidden visibility. There is such a flag in build/common.gypi
ToLocalChecked()
You'll see a few of these calls in the hello_world example:
Local<String> source = String::NewFromUtf8(isolate, js, NewStringType::kNormal).ToLocalChecked();
NewFromUtf8 actually returns a Local
The following is after running the preprocessor (clang -E src/api.cc):
# 5961 "src/api.cc"
Local<String> String::NewFromUtf8(Isolate* isolate,
const char* data,
NewStringType type,
int length) {
MaybeLocal<String> result;
if (length == 0) {
result = String::Empty(isolate);
} else if (length > i::String::kMaxLength) {
result = MaybeLocal<String>();
} else {
i::Isolate* i_isolate = reinterpret_cast<internal::Isolate*>(isolate);
i::VMState<v8::OTHER> __state__((i_isolate));
i::RuntimeCallTimerScope _runtime_timer( i_isolate, &i::RuntimeCallStats::API_String_NewFromUtf8);
LOG(i_isolate, ApiEntryCall("v8::" "String" "::" "NewFromUtf8"));
if (length < 0) length = StringLength(data);
i::Handle<i::String> handle_result = NewString(i_isolate->factory(), static_cast<v8::NewStringType>(type), i::Vector<const char>(data, length)) .ToHandleChecked();
result = Utils::ToLocal(handle_result);
};
return result.FromMaybe(Local<String>());;
}
I was wondering where the Utils::ToLocal was defined but could not find it until I found:
MAKE_TO_LOCAL(ToLocal, String, String)
#define MAKE_TO_LOCAL(Name, From, To) \
Local<v8::To> Utils::Name(v8::internal::Handle<v8::internal::From> obj) { \
return Convert<v8::internal::From, v8::To>(obj); \
}
The above can be found in src/api.h. The same goes for Local<Object>, Local<String> etc.
Small Integers
Reading through v8.h I came accross // Tag information for Smi
Smi stands for small integers.
A pointer is really just a integer that is treated like a memory address. We can use that memory address to get the start of the data located in that memory slot. But we can also just store an normal value like 18 in it. There might be cases where it does not make sense to store a small integer somewhere in the heap and have a pointer to it, but instead store the value directly in the pointer itself. But that only works for small integers so there needs to be away to know if the value we want is stored in the pointer or if we should follow the value stored to the heap to get the value.
A word on a 64 bit machine is 8 bytes (64 bits) and all of the pointers need to be aligned to multiples of 8. So a pointer could be:
1000 = 8
10000 = 16
11000 = 24
100000 = 32
1000000000 = 512
Remember that we are talking about the pointers and not the values store at the memory location they point to. We can see that there are always three bits that are zero in the pointers. So we can use them for something else and just mask them out when using them as pointers.
Tagging involves borrowing one bit of the 32-bit, making it 31-bit and having the leftover bit represent a tag. If the tag is zero then this is a plain value, but if tag is 1 then the pointer must be followed. This does not only have to be for numbers it could also be used for object (I think)
Instead the small integer is represented by the 32 bits plus a pointer to the 64-bit number. V8 needs to know if a value stored in memory represents a 32-bit integer, or if it is really a 64-bit number, in which case it has to follow the pointer to get the complete value. This is where the concept of tagging comes in.
Properties/Elements
Take the following object:
{ firstname: "Jon", lastname: "Doe' }
The above object has two named properties. Named properties differ from integer indexed which is what you have when you are working with arrays.
Memory layout of JavaScript Object:
Properties JavaScript Object Elements
+-----------+ +-----------------+ +----------------+
|property1 |<------+ | HiddenClass | +----->| |
+-----------+ | +-----------------+ | +----------------+
|... | +------| Properties | | | element1 |<------+
+-----------+ +-----------------+ | +----------------+ |
|... | | Elements |--+ | ... | |
+-----------+ +-----------------+ +----------------+ |
|propertyN | <---------------------+ | elementN | |
+-----------+ | +----------------+ |
| |
| |
| |
Named properties: { firstname: "Jon", lastname: "Doe' } Indexed Properties: {1: "Jon", 2: "Doe"}
We can see that properies and elements are stored in different data structures. Elements are usually implemented as a plain array and the indexes can be used for fast access to the elements. But for the properties this is not the case. Instead there is a mapping between the property names and the index into the properties.
In src/objects/objects.h we can find JSObject:
class JSObject: public JSReceiver {
...
DECL_ACCESSORS(elements, FixedArrayBase)
And looking a the DECL_ACCESSOR macro:
#define DECL_ACCESSORS(name, type) \
inline type* name() const; \
inline void set_##name(type* value, \
WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
inline FixedArrayBase* name() const;
inline void set_elements(FixedArrayBase* value, WriteBarrierMode = UPDATE_WRITE_BARRIER)
Notice that JSObject extends JSReceiver which is extended by all types that can have properties defined on them. I think this includes all JSObjects and JSProxy. It is in JSReceiver that the we find the properties array:
DECL_ACCESSORS(raw_properties_or_hash, Object)
Now properties (named properties not elements) can be of different kinds internally. These work just like simple dictionaries from the outside but a dictionary is only used in certain curcumstances at runtime.
Properties JSObject HiddenClass (Map)
+-----------+ +-----------------+ +----------------+
|property1 |<------+ | HiddenClass |-------->| bit field1 |
+-----------+ | +-----------------+ +----------------+
|... | +------| Properties | | bit field2 |
+-----------+ +-----------------+ +----------------+
|... | | Elements | | bit field3 |
+-----------+ +-----------------+ +----------------+
|propertyN | | property1 |
+-----------+ +-----------------+
| property2 |
+-----------------+
| ... |
+-----------------+
JSObject
Each JSObject has as its first field a pointer to the generated HiddenClass. A hiddenclass contain mappings from property names to indices into the properties data type. When an instance of JSObject is created a Map is passed in.
As mentioned earlier JSObject inherits from JSReceiver which inherits from HeapObject
For example,in jsobject_test.cc we first create a new Map using the internal Isolate Factory:
v8::internal::Handle<v8::internal::Map> map = factory->NewMap(v8::internal::JS_OBJECT_TYPE, 24);
v8::internal::Handle<v8::internal::JSObject> js_object = factory->NewJSObjectFromMap(map);
EXPECT_TRUE(js_object->HasFastProperties());
When we call js_object->HasFastProperties() this will delegate to the map instance:
return !map()->is_dictionary_map();
How do you add a property to a JSObject instance? Take a look at jsobject_test.cc for an example.
Caching
Are ways to optimize polymorphic function calls in dynamic languages, for example JavaScript.
Lookup caches
Sending a message to a receiver requires the runtime to find the correct target method using the runtime type of the receiver. A lookup cache maps the type of the receiver/message name pair to methods and stores the most recently used lookup results. The cache is first consulted and if there is a cache miss a normal lookup is performed and the result stored in the cache.
Inline caches
Using a lookup cache as described above still takes a considerable amount of time since the cache must be probed for each message. It can be observed that the type of the target does often not vary. If a call to type A is done at a particular call site it is very likely that the next time it is called the type will also be A. The method address looked up by the system lookup routine can be cached and the call instruction can be overwritten. Subsequent calls for the same type can jump directly to the cached method and completely avoid the lookup. The prolog of the called method must verify that the receivers type has not changed and do the lookup if it has changed (the type if incorrect, no longer A for example).
The target methods address is stored in the callers code, or "inline" with the callers code, hence the name "inline cache".
If V8 is able to make a good assumption about the type of object that will be passed to a method, it can bypass the process of figuring out how to access the objects properties, and instead use the stored information from previous lookups to the objects hidden class.
Polymorfic Inline cache (PIC)
A polymorfic call site is one where there are many equally likely receiver types (and thus call targets).
- Monomorfic means there is only one receiver type
- Polymorfic a few receiver types
- Megamorfic very many receiver types
This type of caching extends inline caching to not just cache the last lookup, but cache all lookup results for a given polymorfic call site using a specially generated stub. Lets say we have a method that iterates through a list of types and calls a method. If all the types are the same (monomorfic) a PIC acts just like an inline cache. The calls will directly call the target method (with the method prolog followed by the method body). If a different type exists in the list there will be a cache miss in the prolog and the lookup routine called. In normal inline caching this would rebind the call, replacing the call to this types target method. This would happen each time the type changes.
With PIC the cache miss handler will generate a small stub routine and rebinds the call to this stub. The stub will check if the receiver is of a type that it has seen before and branch to the correct targets. Since the type of the target is already known at this point it can directly branch to the target method body without the need for the prolog. If the type has not been seen before it will be added to the stub to handle that type. Eventually the stub will contain all types used and there will be no more cache misses/lookups.
The problem is that we don't have type information so methods cannot be called directly, but instead be looked up. In a static language a virtual table might have been used. In JavaScript there is no inheritance relationship so it is not possible to know a vtable offset ahead of time. What can be done is to observe and learn about the "types" used in the program. When an object is seen it can be stored and the target of that method call can be stored and inlined into that call. Bascially the type will be checked and if that particular type has been seen before the method can just be invoked directly. But how do we check the type in a dynamic language? The answer is hidden classes which allow the VM to quickly check an object against a hidden class.
The inline caching source are located in src/ic.
--trace-ic
$ out/x64.debug/d8 --trace-ic --trace-maps class.js
before
[TraceMaps: Normalize from= 0x19a314288b89 to= 0x19a31428aff9 reason= NormalizeAsPrototype ]
[TraceMaps: ReplaceDescriptors from= 0x19a31428aff9 to= 0x19a31428b051 reason= CopyAsPrototype ]
[TraceMaps: InitialMap map= 0x19a31428afa1 SFI= 34_Person ]
[StoreIC in ~Person+65 at class.js:2 (0->.) map=0x19a31428afa1 0x10e68ba83361 <String[4]: name>]
[TraceMaps: Transition from= 0x19a31428afa1 to= 0x19a31428b0a9 name= name ]
[StoreIC in ~Person+102 at class.js:3 (0->.) map=0x19a31428b0a9 0x2beaa25abd89 <String[3]: age>]
[TraceMaps: Transition from= 0x19a31428b0a9 to= 0x19a31428b101 name= age ]
[TraceMaps: SlowToFast from= 0x19a31428b051 to= 0x19a31428b159 reason= OptimizeAsPrototype ]
[StoreIC in ~Person+65 at class.js:2 (.->1) map=0x19a31428afa1 0x10e68ba83361 <String[4]: name>]
[StoreIC in ~Person+102 at class.js:3 (.->1) map=0x19a31428b0a9 0x2beaa25abd89 <String[3]: age>]
[LoadIC in ~+546 at class.js:9 (0->.) map=0x19a31428b101 0x10e68ba83361 <String[4]: name>]
[CallIC in ~+571 at class.js:9 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
Daniel
[LoadIC in ~+642 at class.js:10 (0->.) map=0x19a31428b101 0x2beaa25abd89 <String[3]: age>]
[CallIC in ~+667 at class.js:10 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
41
[LoadIC in ~+738 at class.js:11 (0->.) map=0x19a31428b101 0x10e68ba83361 <String[4]: name>]
[CallIC in ~+763 at class.js:11 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
Tilda
[LoadIC in ~+834 at class.js:12 (0->.) map=0x19a31428b101 0x2beaa25abd89 <String[3]: age>]
[CallIC in ~+859 at class.js:12 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
2
[CallIC in ~+927 at class.js:13 (0->1) map=0x0 0x32f481082231 <String[5]: print>]
after
LoadIC (0->.) means that it has transitioned from unititialized state (0) to pre-monomophic state (.)
monomorphic state is specified with a 1. These states can be found in src/ic/ic.cc.
What we are doing caching knowledge about the layout of the previously seen object inside the StoreIC/LoadIC calls.
$ lldb -- out/x64.debug/d8 class.js
HeapObject
This class describes heap allocated objects. It is in this class we find
information regarding the type of object. This information is contained in
v8::internal::Map.
v8::internal::Map
src/objects/map.h
bit_field1bit_field2bit field3contains information about the number of properties that this Map has, a pointer to an DescriptorArray. The DescriptorArray contains information like the name of the property, and the posistion where the value is stored in the JSObject. I noticed that this information available in src/objects/map.h.
DescriptorArray
Can be found in src/objects/descriptor-array.h. This class extends FixedArray and has the following entries:
[0] the number of descriptors it contains
[1] If uninitialized this will be Smi(0) otherwise an enum cache bridge which is a FixedArray of size 2:
[0] enum cache: FixedArray containing all own enumerable keys
[1] either Smi(0) or a pointer to a FixedArray with indices
[2] first key (and internalized String
[3] first descriptor
Factory
Each Internal Isolate has a Factory which is used to create instances. This is because all handles needs to be allocated using the factory (src/heap/factory.h)
Objects
All objects extend the abstract class Object (src/objects/objects.h).
Oddball
This class extends HeapObject and describes null, undefined, true, and
false objects.
Map
Extends HeapObject and all heap objects have a Map which describes the objects structure. This is where you can find the size of the instance, access to the inobject_properties.
Compiler pipeline
When a script is compiled all of the top level code is parsed. These are function declarartions (but not the function bodies).
function f1() { <- top level code
console.log('f1'); <- non top level
}
function f2() { <- top level code
f1(); <- non top level
console.logg('f2'); <- non top level
}
f2(); <- top level code
var i = 10; <- top level code
The non top level code must be pre-parsed to check for syntax errors. The top level code is parsed and compiles by the full-codegen compiler. This compiler does not perform any optimizations and it's only task is to generate machine code as quickly as possible (this is pre turbofan)
Source ------> Parser --------> Full-codegen ---------> Unoptimized Machine Code
So the whole script is parsed even though we only generated code for the top-level code. The pre-parse (the syntax checking) was not stored in any way. The functions are lazy stubs that when/if the function gets called the function get compiled. This means that the function has to be parsed (again, the first time was the pre-parse remember).
If a function is determined to be hot it will be optimized by one of the two optimizing compilers crankshaft for older parts of JavaScript or Turbofan for Web Assembly (WASM) and some of the newer es6 features.
The first time V8 sees a function it will parse it into an AST but not do any further processing of that tree until that function is used.
+-----> Full-codegen -----> Unoptimized code
/ \/ /\ \
Parser ------> AST -------> Cranshaft -----> Optimized code |
\ /
+-----> Turbofan -----> Optimized code
Inline Cachine (IC) is done here which also help to gather type information. V8 also has a profiler thread which monitors which functions are hot and should be optimized. This profiling also allows V8 to find out information about types using IC. This type information can then be fed to Crankshaft/Turbofan. The type information is stored as a 8 bit value.
When a function is optimized the unoptimized code cannot be thrown away as it might be needed since JavaScript is highly dynamic the optimzed function migth change and the in that case we fallback to the unoptimzed code. This takes up alot of memory which may be important for low end devices. Also the time spent in parsing (twice) takes time.
The idea with Ignition is to be an bytecode interpreter and to reduce memory consumption, the bytecode is very consice compared to native code which can vary depending on the target platform. The whole source can be parsed and compiled, compared to the current pipeline the has the pre-parse and parse stages mentioned above. So even unused functions will get compiled. The bytecode becomes the source of truth instead of as before the AST.
Source ------> Parser --------> Ignition-codegen ---------> Bytecode ---------> Turbofan ----> Optimized Code ---+
/\ |
+--------------------------------------------------+
function bajja(a, b, c) {
var d = c - 100;
return a + d * b;
}
var result = bajja(2, 2, 150);
print(result);
$ ./d8 test.js --ignition --print_bytecode
[generating bytecode for function: bajja]
Parameter count 4
Frame size 8
14 E> 0x2eef8d9b103e @ 0 : 7f StackCheck
38 S> 0x2eef8d9b103f @ 1 : 03 64 LdaSmi [100] // load 100
38 E> 0x2eef8d9b1041 @ 3 : 2b 02 02 Sub a2, [2] // a2 is the third argument. a2 is an argument register
0x2eef8d9b1044 @ 6 : 1f fa Star r0 // r0 is a register for local variables. We only have one which is d
47 S> 0x2eef8d9b1046 @ 8 : 1e 03 Ldar a1 // LoaD accumulator from Register argument a1 which is b
60 E> 0x2eef8d9b1048 @ 10 : 2c fa 03 Mul r0, [3] // multiply that is our local variable in r0
56 E> 0x2eef8d9b104b @ 13 : 2a 04 04 Add a0, [4] // add that to our argument register 0 which is a
65 S> 0x2eef8d9b104e @ 16 : 83 Return // return the value in the accumulator?
Abstract Syntax Tree (AST)
In src/ast/ast.h. You can print the ast using the --print-ast option for d8.
Lets take the following javascript and look at the ast:
const msg = 'testing';
console.log(msg);
$ d8 --print-ast simple.js
[generating interpreter code for user-defined function: ]
--- AST ---
FUNC at 0
. KIND 0
. SUSPEND COUNT 0
. NAME ""
. INFERRED NAME ""
. DECLS
. . VARIABLE (0x7ffe5285b0f8) (mode = CONST) "msg"
. BLOCK NOCOMPLETIONS at -1
. . EXPRESSION STATEMENT at 12
. . . INIT at 12
. . . . VAR PROXY context[4] (0x7ffe5285b0f8) (mode = CONST) "msg"
. . . . LITERAL "testing"
. EXPRESSION STATEMENT at 23
. . ASSIGN at -1
. . . VAR PROXY local[0] (0x7ffe5285b330) (mode = TEMPORARY) ".result"
. . . CALL Slot(0)
. . . . PROPERTY Slot(4) at 31
. . . . . VAR PROXY Slot(2) unallocated (0x7ffe5285b3d8) (mode = DYNAMIC_GLOBAL) "console"
. . . . . NAME log
. . . . VAR PROXY context[4] (0x7ffe5285b0f8) (mode = CONST) "msg"
. RETURN at -1
. . VAR PROXY local[0] (0x7ffe5285b330) (mode = TEMPORARY) ".result"
You can find the declaration of EXPRESSION in ast.h.
Bytecode
Can be found in src/interpreter/bytecodes.h
- StackCheck checks that stack limits are not exceeded to guard against overflow.
StarStore content in accumulator regiser in register (the operand).- Ldar LoaD accumulator from Register argument a1 which is b
The registers are not machine registers, apart from the accumlator as I understand it, but would instead be stack allocated.
Parsing
Parsing is the parsing of the JavaScript and the generation of the abstract syntax tree. That tree is then visited and bytecode generated from it. This section tries to figure out where in the code these operations are performed.
For example, take the script example.
$ make run-script
$ lldb -- run-script
(lldb) br s -n main
(lldb) r
Lets take a look at the following line:
Local<Script> script = Script::Compile(context, source).ToLocalChecked();
This will land us in api.cc
ScriptCompiler::Source script_source(source);
return ScriptCompiler::Compile(context, &script_source);
MaybeLocal<Script> ScriptCompiler::Compile(Local<Context> context, Source* source, CompileOptions options) {
...
auto isolate = context->GetIsolate();
auto maybe = CompileUnboundInternal(isolate, source, options);
CompileUnboundInternal will call GetSharedFunctionInfoForScript (in src/compiler.cc):
result = i::Compiler::GetSharedFunctionInfoForScript(
str, name_obj, line_offset, column_offset, source->resource_options,
source_map_url, isolate->native_context(), NULL, &script_data, options,
i::NOT_NATIVES_CODE);
(lldb) br s -f compiler.cc -l 1259
LanguageMode language_mode = construct_language_mode(FLAG_use_strict);
(lldb) p language_mode
(v8::internal::LanguageMode) \$10 = SLOPPY
LanguageMode can be found in src/globals.h and it is an enum with three values:
enum LanguageMode : uint32_t { SLOPPY, STRICT, LANGUAGE_END };
SLOPPY mode, I assume, is the mode when there is no "use strict";. Remember that this can go inside a function and does not
have to be at the top level of the file.
ParseInfo parse_info(script);
There is a unit test that shows how a ParseInfo instance can be created and inspected.
This will call ParseInfo's constructor (in src/parsing/parse-info.cc), and which will call ParseInfo::InitFromIsolate:
DCHECK_NOT_NULL(isolate);
set_hash_seed(isolate->heap()->HashSeed());
set_stack_limit(isolate->stack_guard()->real_climit());
set_unicode_cache(isolate->unicode_cache());
set_runtime_call_stats(isolate->counters()->runtime_call_stats());
set_ast_string_constants(isolate->ast_string_constants());
I was curious about these ast_string_constants:
(lldb) p *ast_string_constants_
(const v8::internal::AstStringConstants) \$58 = {
zone_ = {
allocation_size_ = 1312
segment_bytes_allocated_ = 8192
position_ = 0x0000000105052538 <no value available>
limit_ = 0x0000000105054000 <no value available>
allocator_ = 0x0000000103e00080
segment_head_ = 0x0000000105052000
name_ = 0x0000000101623a70 "../../src/ast/ast-value-factory.h:365"
sealed_ = false
}
string_table_ = {
v8::base::TemplateHashMapImpl<void *, void *, v8::base::HashEqualityThenKeyMatcher<void *, bool (*)(void *, void *)>, v8::base::DefaultAllocationPolicy> = {
map_ = 0x0000000105054000
capacity_ = 64
occupancy_ = 41
match_ = {
match_ = 0x000000010014b260 (libv8.dylib`v8::internal::AstRawString::Compare(void*, void*) at ast-value-factory.cc:122)
}
}
}
hash_seed_ = 500815076
anonymous_function_string_ = 0x0000000105052018
arguments_string_ = 0x0000000105052038
async_string_ = 0x0000000105052058
await_string_ = 0x0000000105052078
boolean_string_ = 0x0000000105052098
constructor_string_ = 0x00000001050520b8
default_string_ = 0x00000001050520d8
done_string_ = 0x00000001050520f8
dot_string_ = 0x0000000105052118
dot_for_string_ = 0x0000000105052138
dot_generator_object_string_ = 0x0000000105052158
dot_iterator_string_ = 0x0000000105052178
dot_result_string_ = 0x0000000105052198
dot_switch_tag_string_ = 0x00000001050521b8
dot_catch_string_ = 0x00000001050521d8
empty_string_ = 0x00000001050521f8
eval_string_ = 0x0000000105052218
function_string_ = 0x0000000105052238
get_space_string_ = 0x0000000105052258
length_string_ = 0x0000000105052278
let_string_ = 0x0000000105052298
name_string_ = 0x00000001050522b8
native_string_ = 0x00000001050522d8
new_target_string_ = 0x00000001050522f8
next_string_ = 0x0000000105052318
number_string_ = 0x0000000105052338
object_string_ = 0x0000000105052358
proto_string_ = 0x0000000105052378
prototype_string_ = 0x0000000105052398
return_string_ = 0x00000001050523b8
set_space_string_ = 0x00000001050523d8
star_default_star_string_ = 0x00000001050523f8
string_string_ = 0x0000000105052418
symbol_string_ = 0x0000000105052438
this_string_ = 0x0000000105052458
this_function_string_ = 0x0000000105052478
throw_string_ = 0x0000000105052498
undefined_string_ = 0x00000001050524b8
use_asm_string_ = 0x00000001050524d8
use_strict_string_ = 0x00000001050524f8
value_string_ = 0x0000000105052518
}
So these are constants that are set on the new ParseInfo instance using the values from the isolate. Not exactly sure what I want with this but I might come back to it later. So, we are back in ParseInfo's constructor:
set_allow_lazy_parsing();
set_toplevel();
set_script(script);
Script is of type v8::internal::Script which can be found in src/object/script.h
Back now in compiler.cc and the GetSharedFunctionInfoForScript function:
Zone compile_zone(isolate->allocator(), ZONE_NAME);
...
if (parse_info->literal() == nullptr && !parsing::ParseProgram(parse_info, isolate))
ParseProgram:
Parser parser(info);
...
FunctionLiteral* result = nullptr;
result = parser.ParseProgram(isolate, info);
parser.ParseProgram:
Handle<String> source(String::cast(info->script()->source()));
(lldb) job *source
"var user1 = new Person('Fletch');\x0avar user2 = new Person('Dr.Rosen');\x0aprint("user1 = " + user1.name);\x0aprint("user2 = " + user2.name);\x0a\x0a"
So here we can see our JavaScript as a String.
std::unique_ptr<Utf16CharacterStream> stream(ScannerStream::For(source));
scanner_.Initialize(stream.get(), info->is_module());
result = DoParseProgram(info);
DoParseProgram:
(lldb) br s -f parser.cc -l 639
...
this->scope()->SetLanguageMode(info->language_mode());
ParseStatementList(body, Token::EOS, &ok);
This call will land in parser-base.h and its ParseStatementList function.
(lldb) br s -f parser-base.h -l 4695
StatementT stat = ParseStatementListItem(CHECK_OK_CUSTOM(Return, kLazyParsingComplete));
result = CompileToplevel(&parse_info, isolate, Handle<SharedFunctionInfo>::null());
This will land in CompileTopelevel (in the same file which is src/compiler.cc):
// Compile the code.
result = CompileUnoptimizedCode(parse_info, shared_info, isolate);
This will land in CompileUnoptimizedCode (in the same file which is src/compiler.cc):
// Prepare and execute compilation of the outer-most function.
std::unique_ptr<CompilationJob> outer_job(
PrepareAndExecuteUnoptimizedCompileJob(parse_info, parse_info->literal(),
shared_info, isolate));
std::unique_ptr<CompilationJob> job(
interpreter::Interpreter::NewCompilationJob(parse_info, literal, isolate));
if (job->PrepareJob() == CompilationJob::SUCCEEDED &&
job->ExecuteJob() == CompilationJob::SUCCEEDED) {
return job;
}
PrepareJobImpl:
CodeGenerator::MakeCodePrologue(parse_info(), compilation_info(),
"interpreter");
return SUCCEEDED;
codegen.cc MakeCodePrologue:
interpreter.cc ExecuteJobImpl:
generator()->GenerateBytecode(stack_limit());
src/interpreter/bytecode-generator.cc
RegisterAllocationScope register_scope(this);
The bytecode is register based (if that is the correct term) and we had an example previously. I'm guessing that this is what this call is about.
VisitDeclarations will iterate over all the declarations in the file which in our case are:
var user1 = new Person('Fletch');
var user2 = new Person('Dr.Rosen');
(lldb) p *variable->raw_name()
(const v8::internal::AstRawString) \$33 = {
= {
next_ = 0x000000010600a280
string_ = 0x000000010600a280
}
literal_bytes_ = (start_ = "user1", length_ = 5)
hash_field_ = 1303438034
is_one_byte_ = true
has_string_ = false
}
// Perform a stack-check before the body.
builder()->StackCheck(info()->literal()->start_position());
So that call will output a stackcheck instruction, like in the example above:
14 E> 0x2eef8d9b103e @ 0 : 7f StackCheck
Performance
Say you have the expression x + y the full-codegen compiler might produce:
movq rax, x
movq rbx, y
callq RuntimeAdd
If x and y are integers just using the add operation would be much quicker:
movq rax, x
movq rbx, y
add rax, rbx
Recall that functions are optimized so if the compiler has to bail out and unoptimize part of a function then the whole functions will be affected and it will go back to the unoptimized version.
Bytecode
This section will examine the bytecode for the following JavaScript:
function beve() {
const p = new Promise((resolve, reject) => {
resolve('ok');
});
p.then(msg => {
console.log(msg);
});
}
beve();
$ d8 --print-bytecode promise.js
First have the main function which does not have a name:
[generating bytecode for function: ]
(The code that generated this can be found in src/objects.cc BytecodeArray::Dissassemble)
Parameter count 1
Frame size 32
// load what ever the FixedArray[4] is in the constant pool into the accumulator.
0x34423e7ac19e @ 0 : 09 00 LdaConstant [0]
// store the FixedArray[4] in register r1
0x34423e7ac1a0 @ 2 : 1e f9 Star r1
// store zero into the accumulator.
0x34423e7ac1a2 @ 4 : 02 LdaZero
// store zero (the contents of the accumulator) into register r2.
0x34423e7ac1a3 @ 5 : 1e f8 Star r2
//
0x34423e7ac1a5 @ 7 : 1f fe f7 Mov <closure>, r3
0x34423e7ac1a8 @ 10 : 53 96 01 f9 03 CallRuntime [DeclareGlobalsForInterpreter], r1-r3
0 E> 0x34423e7ac1ad @ 15 : 90 StackCheck
141 S> 0x34423e7ac1ae @ 16 : 0a 01 00 LdaGlobal [1], [0]
0x34423e7ac1b1 @ 19 : 1e f9 Star r1
141 E> 0x34423e7ac1b3 @ 21 : 4f f9 03 CallUndefinedReceiver0 r1, [3]
0x34423e7ac1b6 @ 24 : 1e fa Star r0
148 S> 0x34423e7ac1b8 @ 26 : 94 Return
Constant pool (size = 2)
0x34423e7ac149: [FixedArray] in OldSpace
- map = 0x344252182309 <Map(HOLEY_ELEMENTS)>
- length: 2
0: 0x34423e7ac069 <FixedArray[4]>
1: 0x34423e7abf59 <String[4]: beve>
Handler Table (size = 16) Load the global with name in constant pool entry <name_index> into the
// accumulator using FeedBackVector slot <slot> outside of a typeof
- LdaConstant
Load the constant at index from the constant pool into the accumulator. - Star
Store the contents of the accumulator register in dst. - Ldar
Load accumulator with value from register src. - LdaGlobal
Load the global with name in constant pool entry idx into the accumulator using FeedBackVector slot outside of a typeof. - Mov
, Store the value of register
You can find the declarations for the these instructions in src/interpreter/interpreter-generator.cc.
Unified code generation architecture
FeedbackVector
Is attached to every function and is responsible for recording and managing all execution feedback, which is information about types enabling.
You can find the declaration for this class in src/feedback-vector.h
BytecodeGenerator
Is currently the only part of V8 that cares about the AST.
BytecodeGraphBuilder
Produces high-level IR graph based on interpreter bytecodes.
TurboFan
Is a compiler backend that gets fed a control flow graph and then does instruction selection, register allocation and code generation. The code generation generates
Execution/Runtime
I'm not sure if V8 follows this exactly but I've heard and read that when the engine comes across a function declaration it only parses and verifies the syntax and saves a ref to the function name. The statements inside the function are not checked at this stage only the syntax of the function declaration (parenthesis, arguments, brackets etc).
Function methods
The declaration of Function can be found in include/v8.h (just noting this as I've looked for it several times)
Symbol
The declarations for the Symbol class can be found in v8.h and the internal
implementation in src/api/api.cc.
The well known Symbols are generated using macros so you won't find the just by searching using the static function names like 'GetToPrimitive`.
#define WELL_KNOWN_SYMBOLS(V) \
V(AsyncIterator, async_iterator) \
V(HasInstance, has_instance) \
V(IsConcatSpreadable, is_concat_spreadable) \
V(Iterator, iterator) \
V(Match, match) \
V(Replace, replace) \
V(Search, search) \
V(Split, split) \
V(ToPrimitive, to_primitive) \
V(ToStringTag, to_string_tag) \
V(Unscopables, unscopables)
#define SYMBOL_GETTER(Name, name) \
Local<Symbol> v8::Symbol::Get##Name(Isolate* isolate) { \
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate); \
return Utils::ToLocal(i_isolate->factory()->name##_symbol()); \
}
So GetToPrimitive would become:
Local<Symbol> v8::Symbol::GeToPrimitive(Isolate* isolate) {
i::Isolate* i_isolate = reinterpret_cast<i::Isolate*>(isolate);
return Utils::ToLocal(i_isolate->factory()->to_primitive_symbol());
}
There is an example in symbol-test.cc.
Builtins
Are JavaScript functions/objects that are provided by V8. These are built using a C++ DSL and are passed through:
CodeStubAssembler -> CodeAssembler -> RawMachineAssembler.
Builtins need to have bytecode generated for them so that they can be run in TurboFan.
src/code-stub-assembler.h
All the builtins are declared in src/builtins/builtins-definitions.h by the
BUILTIN_LIST_BASE macro.
There are different type of builtins (TF = Turbo Fan):
-
TFJ JavaScript linkage which means it is callable as a JavaScript function
-
TFS CodeStub linkage. A builtin with stub linkage can be used to extract common code into a separate code object which can then be used by multiple callers. These is useful because builtins are generated at compile time and included in the V8 snapshot. This means that they are part of every isolate that is created. Being able to share common code for multiple builtins will save space.
-
TFC CodeStub linkage with custom descriptor
To see how this works in action we first need to disable snapshots. If we don't, we won't be able to set breakpoints as the the heap will be serialized at compile time and deserialized upon startup of v8.
To find the option to disable snapshots use:
$ gn args --list out.gn/learning --short | more
...
v8_use_snapshot=true
$ gn args out.gn/learning
v8_use_snapshot=false
$ gn -C out.gn/learning
After building we should be able to set a break point in bootstrapper.cc and its function
Genesis::InitializeGlobal:
(lldb) br s -f bootstrapper.cc -l 2684
Lets take a look at how the JSON object is setup:
Handle<String> name = factory->InternalizeUtf8String("JSON");
Handle<JSObject> json_object = factory->NewJSObject(isolate->object_function(), TENURED);
TENURED means that this object should be allocated directly in the old generation.
JSObject::AddProperty(global, name, json_object, DONT_ENUM);
DONT_ENUM is checked by some builtin functions and if set this object will be ignored by those
functions.
SimpleInstallFunction(json_object, "parse", Builtins::kJsonParse, 2, false);
Here we can see that we are installing a function named parse, which takes 2 parameters. You can
find the definition in src/builtins/builtins-json.cc.
What does the SimpleInstallFunction do?
Lets take console as an example which was created using:
Handle<JSObject> console = factory->NewJSObject(cons, TENURED);
JSObject::AddProperty(global, name, console, DONT_ENUM);
SimpleInstallFunction(console, "debug", Builtins::kConsoleDebug, 1, false,
NONE);
V8_NOINLINE Handle<JSFunction> SimpleInstallFunction(
Handle<JSObject> base,
const char* name,
Builtins::Name call,
int len,
bool adapt,
PropertyAttributes attrs = DONT_ENUM,
BuiltinFunctionId id = kInvalidBuiltinFunctionId) {
So we can see that base is our Handle to a JSObject, and name is "debug".
Builtins::Name is Builtins:kConsoleDebug. Where is this defined?
You can find a macro named CPP in src/builtins/builtins-definitions.h:
CPP(ConsoleDebug)
What does this macro expand to?
It is part of the BUILTIN_LIST_BASE macro in builtin-definitions.h
We have to look at where BUILTIN_LIST is used which we can find in builtins.cc.
In builtins.cc we have an array of BuiltinMetadata which is declared as:
const BuiltinMetadata builtin_metadata[] = {
BUILTIN_LIST(DECL_CPP, DECL_API, DECL_TFJ, DECL_TFC, DECL_TFS, DECL_TFH, DECL_ASM)
};
#define DECL_CPP(Name, ...) { #Name, Builtins::CPP, \
{ FUNCTION_ADDR(Builtin_##Name) }},
Which will expand to the creation of a BuiltinMetadata struct entry in the array. The BuildintMetadata struct looks like this which might help understand what is going on:
struct BuiltinMetadata {
const char* name;
Builtins::Kind kind;
union {
Address cpp_entry; // For CPP and API builtins.
int8_t parameter_count; // For TFJ builtins.
} kind_specific_data;
};
So the CPP(ConsoleDebug) will expand to an entry in the array which would look something like
this:
{ ConsoleDebug,
Builtins::CPP,
{
reinterpret_cast<v8::internal::Address>(reinterpret_cast<intptr_t>(Builtin_ConsoleDebug))
}
},
The third paramter is the creation on the union which might not be obvious.
Back to the question I'm trying to answer which is:
"Buildtins::Name is is Builtins:kConsoleDebug. Where is this defined?"
For this we have to look at builtins.h and the enum Name:
enum Name : int32_t {
#define DEF_ENUM(Name, ...) k##Name,
BUILTIN_LIST_ALL(DEF_ENUM)
#undef DEF_ENUM
builtin_count
};
This will expand to the complete list of builtins in builtin-definitions.h using the DEF_ENUM macro. So the expansion for ConsoleDebug will look like:
enum Name: int32_t {
...
kDebugConsole,
...
};
So backing up to looking at the arguments to SimpleInstallFunction which are:
SimpleInstallFunction(console, "debug", Builtins::kConsoleDebug, 1, false,
NONE);
V8_NOINLINE Handle<JSFunction> SimpleInstallFunction(
Handle<JSObject> base,
const char* name,
Builtins::Name call,
int len,
bool adapt,
PropertyAttributes attrs = DONT_ENUM,
BuiltinFunctionId id = kInvalidBuiltinFunctionId) {
We know about Builtins::Name, so lets look at len which is one, what is this?
SimpleInstallFunction will call:
Handle<JSFunction> fun =
SimpleCreateFunction(base->GetIsolate(), function_name, call, len, adapt);
len would be used if adapt was true but it is false in our case. This is what it would
be used for if adapt was true:
fun->shared()->set_internal_formal_parameter_count(len);
I'm not exactly sure what adapt is referring to here.
PropertyAttributes is not specified so it will get the default value of DONT_ENUM.
The last parameter which is of type BuiltinFunctionId is not specified either so the
default value of kInvalidBuiltinFunctionId will be used. This is an enum defined in
src/objects/objects.h.
This blog provides an example of adding a function to the String object.
$ out.gn/learning/mksnapshot --print-code > output
You can then see the generated code from this. This will produce a code stub that can be called through C++. Lets update this to have it be called from JavaScript:
Update builtins/builtins-string-get.cc :
TF_BUILTIN(GetStringLength, StringBuiltinsAssembler) {
Node* const str = Parameter(Descriptor::kReceiver);
Return(LoadStringLength(str));
}
We also have to update builtins/builtins-definitions.h:
TFJ(GetStringLength, 0)
And bootstrapper.cc:
SimpleInstallFunction(prototype, "len", Builtins::kGetStringLength, 0, true);
If you now build using 'ninja -C out.gn/learning_v8' you should be able to run d8 and try this out:
d8> const s = 'testing'
undefined
d8> s.len()
7
Now lets take a closer look at the code that is generated for this:
$ out.gn/learning/mksnapshot --print-code > output
Looking at the output generated I was surprised to see two entries for GetStringLength (I changed the name just to make sure there was not something else generating the second one). Why two?
The following uses Intel Assembly syntax which means that no register/immediate prefixes and the first operand is the destination and the second operand the source.
--- Code ---
kind = BUILTIN
name = BeveStringLength
compiler = turbofan
Instructions (size = 136)
0x1fafde09b3a0 0 55 push rbp
0x1fafde09b3a1 1 4889e5 REX.W movq rbp,rsp // movq rsp into rbp
0x1fafde09b3a4 4 56 push rsi // push the value of rsi (first parameter) onto the stack
0x1fafde09b3a5 5 57 push rdi // push the value of rdi (second parameter) onto the stack
0x1fafde09b3a6 6 50 push rax // push the value of rax (accumulator) onto the stack
0x1fafde09b3a7 7 4883ec08 REX.W subq rsp,0x8 // make room for a 8 byte value on the stack
0x1fafde09b3ab b 488b4510 REX.W movq rax,[rbp+0x10] // move the value rpm + 10 to rax
0x1fafde09b3af f 488b58ff REX.W movq rbx,[rax-0x1]
0x1fafde09b3b3 13 807b0b80 cmpb [rbx+0xb],0x80 // IsString(object). compare byte to zero
0x1fafde09b3b7 17 0f8350000000 jnc 0x1fafde09b40d <+0x6d> // jump it carry flag was not set
0x1fafde09b3bd 1d 488b400f REX.W movq rax,[rax+0xf]
0x1fafde09b3c1 21 4989e2 REX.W movq r10,rsp
0x1fafde09b3c4 24 4883ec08 REX.W subq rsp,0x8
0x1fafde09b3c8 28 4883e4f0 REX.W andq rsp,0xf0
0x1fafde09b3cc 2c 4c891424 REX.W movq [rsp],r10
0x1fafde09b3d0 30 488945e0 REX.W movq [rbp-0x20],rax
0x1fafde09b3d4 34 48be0000000001000000 REX.W movq rsi,0x100000000
0x1fafde09b3de 3e 48bad9c228dfa8090000 REX.W movq rdx,0x9a8df28c2d9 ;; object: 0x9a8df28c2d9 <String[101]: CAST(LoadObjectField(object, offset, MachineTypeOf<T>::value)) at ../../src/code-stub-assembler.h:432>
0x1fafde09b3e8 48 488bf8 REX.W movq rdi,rax
0x1fafde09b3eb 4b 48b830726d0a01000000 REX.W movq rax,0x10a6d7230 ;; external reference (check_object_type)
0x1fafde09b3f5 55 40f6c40f testb rsp,0xf
0x1fafde09b3f9 59 7401 jz 0x1fafde09b3fc <+0x5c>
0x1fafde09b3fb 5b cc int3l
0x1fafde09b3fc 5c ffd0 call rax
0x1fafde09b3fe 5e 488b2424 REX.W movq rsp,[rsp]
0x1fafde09b402 62 488b45e0 REX.W movq rax,[rbp-0x20]
0x1fafde09b406 66 488be5 REX.W movq rsp,rbp
0x1fafde09b409 69 5d pop rbp
0x1fafde09b40a 6a c20800 ret 0x8
// this is where we jump to if IsString failed
0x1fafde09b40d 6d 48ba71c228dfa8090000 REX.W movq rdx,0x9a8df28c271 ;; object: 0x9a8df28c271 <String[76]\: CSA_ASSERT failed: IsString(object) [../../src/code-stub-assembler.cc:1498]\n>
0x1fafde09b417 77 e8e4d1feff call 0x1fafde088600 ;; code: BUILTIN
0x1fafde09b41c 7c cc int3l
0x1fafde09b41d 7d cc int3l
0x1fafde09b41e 7e 90 nop
0x1fafde09b41f 7f 90 nop
Safepoints (size = 8)
RelocInfo (size = 7)
0x1fafde09b3e0 embedded object (0x9a8df28c2d9 <String[101]: CAST(LoadObjectField(object, offset, MachineTypeOf<T>::value)) at ../../src/code-stub-assembler.h:432>)
0x1fafde09b3ed external reference (check_object_type) (0x10a6d7230)
0x1fafde09b40f embedded object (0x9a8df28c271 <String[76]\: CSA_ASSERT failed: IsString(object) [../../src/code-stub-assembler.cc:1498]\n>)
0x1fafde09b418 code target (BUILTIN) (0x1fafde088600)
--- End code ---
TF_BUILTIN macro
Is a macro to defining Turbofan (TF) builtins and can be found in builtins/builtins-utils-gen.h
If we take a look at the file src/builtins/builtins-bigint-gen.cc and the following function:
TF_BUILTIN(BigIntToI64, CodeStubAssembler) {
if (!Is64()) {
Unreachable();
return;
}
TNode<Object> value = CAST(Parameter(Descriptor::kArgument));
TNode<Context> context = CAST(Parameter(Descriptor::kContext));
TNode<BigInt> n = ToBigInt(context, value);
TVARIABLE(UintPtrT, var_low);
TVARIABLE(UintPtrT, var_high);
BigIntToRawBytes(n, &var_low, &var_high);
Return(var_low.value());
}
Let's take our GetStringLength example from above and see what this will be expanded to after processing this macro:
$ clang++ --sysroot=build/linux/debian_sid_amd64-sysroot -isystem=./buildtools/third_party/libc++/trunk/include -isystem=buildtools/third_party/libc++/trunk/include -I. -E src/builtins/builtins-bigint-gen.cc > builtins-bigint-gen.cc.pp
static void Generate_BigIntToI64(compiler::CodeAssemblerState* state);
class BigIntToI64Assembler : public CodeStubAssembler {
public:
using Descriptor = Builtin_BigIntToI64_InterfaceDescriptor;
explicit BigIntToI64Assembler(compiler::CodeAssemblerState* state) : CodeStubAssembler(state) {}
void GenerateBigIntToI64Impl();
Node* Parameter(Descriptor::ParameterIndices index) {
return CodeAssembler::Parameter(static_cast<int>(index));
}
};
void Builtins::Generate_BigIntToI64(compiler::CodeAssemblerState* state) {
BigIntToI64Assembler assembler(state);
state->SetInitialDebugInformation("BigIntToI64", "src/builtins/builtins-bigint-gen.cc", 14);
if (Builtins::KindOf(Builtins::kBigIntToI64) == Builtins::TFJ) {
assembler.PerformStackCheck(assembler.GetJSContextParameter());
}
assembler.GenerateBigIntToI64Impl();
}
void BigIntToI64Assembler::GenerateBigIntToI64Impl() {
if (!Is64()) {
Unreachable();
return;
}
TNode<Object> value = Cast(Parameter(Descriptor::kArgument));
TNode<Context> context = Cast(Parameter(Descriptor::kContext));
TNode<BigInt> n = ToBigInt(context, value);
TVariable<UintPtrT> var_low(this);
TVariable<UintPtrT> var_high(this);
BigIntToRawBytes(n, &var_low, &var_high);
Return(var_low.value());
}
From the resulting class you can see how Parameter can be used from within TF_BUILTIN macro.
Building V8
You'll need to have checked out the Google V8 sources to you local file system and build it by following the instructions found here.
Configure v8 build for learning-v8
There is a make target that can generate a build configuration for V8 that is specific to this project. It can be run using the following command:
$ make configure_v8
Then to compile this configuration:
$ make compile_v8
gclient sync
$ gclient sync
Troubleshooting build:
/v8_src/v8/out/x64.release/obj/libv8_monolith.a(eh-frame.o):eh-frame.cc:function v8::internal::EhFrameWriter::WriteEmptyEhFrame(std::__1::basic_ostream<char, std::__1::char_traits<char> >&): error: undefined reference to 'std::__1::basic_ostream<char, std::__1::char_traits<char> >::write(char const*, long)'
clang: error: linker command failed with exit code 1 (use -v to see invocation)
-stdlib=libc++ is llvm's C++ runtime. This runtime has a __1 namespace.
I looks like the static library above was compiled with clangs/llvm's libc++
as we are seeing the __1 namespace.
-stdlib=libstdc++ is GNU's C++ runtime
So we can see that the namespace std::__1 is used which we now
know is the namespace that libc++ which is clangs libc++ library.
I guess we could go about this in two ways, either we can change v8 build of
to use glibc++ when compiling so that the symbols are correct when we want to
link against it, or we can update our linker (ld) to use libc++.
We need to include the correct libraries to link with during linking, which means specifying:
-stdlib=libc++ -Wl,-L$(v8_build_dir)
If we look in $(v8_build_dir) we find libc++.so. We also need to this library
to be found at runtime by the dynamic linker using LD_LIBRARY_PATH:
$ LD_LIBRARY_PATH=../v8_src/v8/out/x64.release/ ./hello-world
Notice that this is using ld from our path. We can tell clang to use a different
search path with the -B option:
$ clang++ --help | grep -- '-B'
-B <dir> Add <dir> to search path for binaries and object files used implicitly
libgcc_s is GCC low level runtime library. I've been confusing this with
glibc++ libraries for some reason but they are not the same.
Running cctest:
$ out.gn/learning/cctest test-heap-profiler/HeapSnapshotRetainedObjectInfo
To get a list of the available tests:
$ out.gn/learning/cctest --list
Checking formating/linting:
$ git cl format
You can then git diff and see the changes.
Running pre-submit checks:
$ git cl presubmit
Then upload using:
$ git cl upload
Build details
So when we run gn it will generate Ninja build file. GN itself is written in C++ but has a python wrapper around it.
A group in gn is just a collection of other targets which enables them to have a name.
So when we run gn there will be a number of .ninja files generated. If we look in the root of the output directory we find two .ninja files:
build.ninja toolchain.ninja
By default ninja will look for build.ninja and when we run ninja we usually
specify the -C out/dir. If no targets are specified on the command line ninja
will execute all outputs unless there is one specified as default. V8 has the
following default target:
default all
build all: phony $
./bytecode_builtins_list_generator $
./d8 $
obj/fuzzer_support.stamp $
./gen-regexp-special-case $
obj/generate_bytecode_builtins_list.stamp $
obj/gn_all.stamp $
obj/json_fuzzer.stamp $
obj/lib_wasm_fuzzer_common.stamp $
./mksnapshot $
obj/multi_return_fuzzer.stamp $
obj/parser_fuzzer.stamp $
obj/postmortem-metadata.stamp $
obj/regexp_builtins_fuzzer.stamp $
obj/regexp_fuzzer.stamp $
obj/run_gen-regexp-special-case.stamp $
obj/run_mksnapshot_default.stamp $
obj/run_torque.stamp $
./torque $
./torque-language-server $
obj/torque_base.stamp $
obj/torque_generated_definitions.stamp $
obj/torque_generated_initializers.stamp $
obj/torque_ls_base.stamp $
./libv8.so.TOC $
obj/v8_archive.stamp $
...
A phony rule can be used to create an alias for other targets.
The $ in ninja is an escape character so in the case of the all target it
escapes the new line, like using \ in a shell script.
Lets take a look at bytecode_builtins_list_generator:
build $:bytecode_builtins_list_generator: phony ./bytecode_builtins_list_generator
The format of the ninja build statement is:
build outputs: rulename inputs
We are again seeing the $ ninja escape character but this time it is escaping
the colon which would otherwise be interpreted as separating file names. The output
in this case is bytecode_builtins_list_generator. And I'm guessing, as I can't
find a connection between ./bytecode_builtins_list_generator and
The default target_out_dir in this case is //out/x64.release_gcc/obj.
The executable in BUILD.gn which generates this does not specify any output
directory so I'm assuming that it the generated .ninja file is place in the
target_out_dir in this case where we can find bytecode_builtins_list_generator.ninja
This file has a label named:
label_name = bytecode_builtins_list_generator
Hmm, notice that in build.ninja there is the following command:
subninja toolchain.ninja
And in toolchain.ninja we have:
subninja obj/bytecode_builtins_list_generator.ninja
This is what is making ./bytecode_builtins_list_generator available.
$ ninja -C out/x64.release_gcc/ -t targets all | grep bytecode_builtins_list_generator
$ rm out/x64.release_gcc/bytecode_builtins_list_generator
$ ninja -C out/x64.release_gcc/ bytecode_builtins_list_generator
ninja: Entering directory `out/x64.release_gcc/'
[1/1] LINK ./bytecode_builtins_list_generator
Alright, so I'd like to understand when in the process torque is run to generate classes like TorqueGeneratedStruct:
class Struct : public TorqueGeneratedStruct<Struct, HeapObject> {
./torque $
./torque-language-server $
obj/torque_base.stamp $
obj/torque_generated_definitions.stamp $
obj/torque_generated_initializers.stamp $
obj/torque_ls_base.stamp $
Like before we can find that obj/torque.ninja in included by the subninja command in toolchain.ninja:
subninja obj/torque.ninja
So this is building the executable torque, but it has not been run yet.
$ gn ls out/x64.release_gcc/ --type=action
//:generate_bytecode_builtins_list
//:postmortem-metadata
//:run_gen-regexp-special-case
//:run_mksnapshot_default
//:run_torque
//:v8_dump_build_config
//src/inspector:protocol_compatibility
//src/inspector:protocol_generated_sources
//tools/debug_helper:gen_heap_constants
//tools/debug_helper:run_mkgrokdump
Notice the run_torque target
$ gn desc out/x64.release_gcc/ //:run_torque
If we look in toolchain.ninja we have a rule named ___run_torque___build_toolchain_linux_x64__rule
command = python ../../tools/run.py ./torque -o gen/torque-generated -v8-root ../..
src/builtins/array-copywithin.tq
src/builtins/array-every.tq
src/builtins/array-filter.tq
src/builtins/array-find.tq
...
And there is a build that specifies the .h and cc files in gen/torque-generated which has this rule in it if they change.
Building chromium
When making changes to V8 you might need to verify that your changes have not broken anything in Chromium.
Generate Your Project (gpy) : You'll have to run this once before building:
$ gclient sync
$ gclient runhooks
Update the code base
$ git fetch origin master
$ git co master
$ git merge origin/master
Building using GN
$ gn args out.gn/learning
Building using Ninja
$ ninja -C out.gn/learning
Building the tests:
$ ninja -C out.gn/learning chrome/test:unit_tests
An error I got when building the first time:
traceback (most recent call last):
File "./gyp-mac-tool", line 713, in <module>
sys.exit(main(sys.argv[1:]))
File "./gyp-mac-tool", line 29, in main
exit_code = executor.Dispatch(args)
File "./gyp-mac-tool", line 44, in Dispatch
return getattr(self, method)(*args[1:])
File "./gyp-mac-tool", line 68, in ExecCopyBundleResource
self._CopyStringsFile(source, dest)
File "./gyp-mac-tool", line 134, in _CopyStringsFile
import CoreFoundation
ImportError: No module named CoreFoundation
[6642/20987] CXX obj/base/debug/base.task_annotator.o
[6644/20987] ACTION base_nacl: build newlib plib_9b4f41e4158ebb93a5d28e6734a13e85
ninja: build stopped: subcommand failed.
I was able to get around this by:
$ pip install -U pyobjc
Using a specific version of V8
The instructions below work but it is also possible to create a soft link from chromium/src/v8 to local v8 repository and the build/test.
So, we want to include our updated version of V8 so that we can verify that it builds correctly with our change to V8. While I'm not sure this is the proper way to do it, I was able to update DEPS in src (chromium) and set the v8 entry to git@github.com:danbev/v8.git@064718a8921608eaf9b5eadbb7d734ec04068a87:
"git@github.com:danbev/v8.git@064718a8921608eaf9b5eadbb7d734ec04068a87"
You'll have to run gclient sync after this.
Another way is to not updated the DEPS file, which is a version controlled file, but instead update
.gclientrc and add a custom_deps entry:
solutions = [{u'managed': False, u'name': u'src', u'url': u'https://chromium.googlesource.com/chromium/src.git',
u'custom_deps': {
"src/v8": "git@github.com:danbev/v8.git@27a666f9be7ca3959c7372bdeeee14aef2a4b7ba"
}, u'deps_file': u'.DEPS.git', u'safesync_url': u''}]
Buiding pdfium
You may have to compile this project (in addition to chromium to verify that changes in v8 are not breaking code in pdfium.
Create/clone the project
$ mkdir pdfuim_reop
$ gclient config --unmanaged https://pdfium.googlesource.com/pdfium.git
$ gclient sync
$ cd pdfium
Building
$ ninja -C out/Default
Using a branch of v8
You should be able to update the .gclient file adding a custom_deps entry:
solutions = [
{
"name" : "pdfium",
"url" : "https://pdfium.googlesource.com/pdfium.git",
"deps_file" : "DEPS",
"managed" : False,
"custom_deps" : {
"v8": "git@github.com:danbev/v8.git@064718a8921608eaf9b5eadbb7d734ec04068a87"
},
},
]
cache_dir = None
You'll have to run gclient sync after this too.
Code in this repo
hello-world
hello-world is heavily commented and show the usage of a static int being exposed and accessed from JavaScript.
instances
instances shows the usage of creating new instances of a C++ class from JavaScript.
run-script
run-script is basically the same as instance but reads an external file, script.js and run the script.
tests
The test directory contains unit tests for individual classes/concepts in V8 to help understand them.
Building this projects code
$ make
Running
$ ./hello-world
Cleaning
$ make clean
Contributing a change to V8
- Create a working branch using
git new-branch name - git cl upload
See Googles contributing-code for more details.
Find the current issue number
$ git cl issue
Debugging
$ lldb hello-world
(lldb) br s -f hello-world.cc -l 27
There are a number of useful functions in src/objects-printer.cc which can also be used in lldb.
Print value of a Local object
(lldb) print _v8_internal_Print_Object(*(v8::internal::Object**)(*init_fn))
Print stacktrace
(lldb) p _v8_internal_Print_StackTrace()
Creating command aliases in lldb
Create a file named .lldbinit (in your project director or home directory). This file can now be found in v8's tools directory.
Using d8
This is the source used for the following examples:
$ cat class.js
function Person(name, age) {
this.name = name;
this.age = age;
}
print("before");
const p = new Person("Daniel", 41);
print(p.name);
print(p.age);
print("after");
V8_shell startup
What happens when the v8_shell is run?
$ lldb -- out/x64.debug/d8 --enable-inspector class.js
(lldb) breakpoint set --file d8.cc --line 2662
Breakpoint 1: where = d8`v8::Shell::Main(int, char**) + 96 at d8.cc:2662, address = 0x0000000100015150
First v8::base::debug::EnableInProcessStackDumping() is called followed by some windows specific code guarded
by macros. Next is all the options are set using v8::Shell::SetOptions
SetOptions will call v8::V8::SetFlagsFromCommandLine which is found in src/api.cc:
i::FlagList::SetFlagsFromCommandLine(argc, argv, remove_flags);
This function can be found in src/flags.cc. The flags themselves are defined in src/flag-definitions.h
Next a new SourceGroup array is create:
options.isolate_sources = new SourceGroup[options.num_isolates];
SourceGroup* current = options.isolate_sources;
current->Begin(argv, 1);
for (int i = 1; i < argc; i++) {
const char* str = argv[i];
(lldb) p str
(const char *) \$6 = 0x00007fff5fbfed4d "manual.js"
There are then checks performed to see if the args is --isolate or --module, or -e and if not (like in our case)
} else if (strncmp(str, "-", 1) != 0) {
// Not a flag, so it must be a script to execute.
options.script_executed = true;
TODO: I'm not exactly sure what SourceGroups are about but just noting this and will revisit later.
This will take us back int Shell::Main in src/d8.cc
::V8::InitializeICUDefaultLocation(argv[0], options.icu_data_file);
(lldb) p argv[0]
(char *) \$8 = 0x00007fff5fbfed48 "./d8"
See ICU a little more details.
Next the default V8 platform is initialized:
g_platform = i::FLAG_verify_predictable ? new PredictablePlatform() : v8::platform::CreateDefaultPlatform();
v8::platform::CreateDefaultPlatform() will be called in our case.
We are then back in Main and have the following lines:
2685 v8::V8::InitializePlatform(g_platform);
2686 v8::V8::Initialize();
This is very similar to what I've seen in the Node.js startup process.
We did not specify any natives_blob or snapshot_blob as an option on the command line so the defaults will be used:
v8::V8::InitializeExternalStartupData(argv[0]);
back in src/d8.cc line 2918:
Isolate* isolate = Isolate::New(create_params);
this call will bring us into api.cc line 8185:
i::Isolate* isolate = new i::Isolate(false);
So, we are invoking the Isolate constructor (in src/isolate.cc).
isolate->set_snapshot_blob(i::Snapshot::DefaultSnapshotBlob());
api.cc:
isolate->Init(NULL);
compilation_cache_ = new CompilationCache(this);
context_slot_cache_ = new ContextSlotCache();
descriptor_lookup_cache_ = new DescriptorLookupCache();
unicode_cache_ = new UnicodeCache();
inner_pointer_to_code_cache_ = new InnerPointerToCodeCache(this);
global_handles_ = new GlobalHandles(this);
eternal_handles_ = new EternalHandles();
bootstrapper_ = new Bootstrapper(this);
handle_scope_implementer_ = new HandleScopeImplementer(this);
load_stub_cache_ = new StubCache(this, Code::LOAD_IC);
store_stub_cache_ = new StubCache(this, Code::STORE_IC);
materialized_object_store_ = new MaterializedObjectStore(this);
regexp_stack_ = new RegExpStack();
regexp_stack_->isolate_ = this;
date_cache_ = new DateCache();
call_descriptor_data_ =
new CallInterfaceDescriptorData[CallDescriptors::NUMBER_OF_DESCRIPTORS];
access_compiler_data_ = new AccessCompilerData();
cpu_profiler_ = new CpuProfiler(this);
heap_profiler_ = new HeapProfiler(heap());
interpreter_ = new interpreter::Interpreter(this);
compiler_dispatcher_ =
new CompilerDispatcher(this, V8::GetCurrentPlatform(), FLAG_stack_size);
src/builtins/builtins.cc, this is where the builtins are defined. TODO: sort out what these macros do.
In src/v8.cc we have a couple of checks for if the options passed are for a stress_run but since we did not pass in any such flags this code path will be followed which will call RunMain:
result = RunMain(isolate, argc, argv, last_run);
this will end up calling:
options.isolate_sources[0].Execute(isolate);
Which will call SourceGroup::Execute(Isolate* isolate)
// Use all other arguments as names of files to load and run.
HandleScope handle_scope(isolate);
Local<String> file_name = String::NewFromUtf8(isolate, arg, NewStringType::kNormal).ToLocalChecked();
Local<String> source = ReadFile(isolate, arg);
if (source.IsEmpty()) {
printf("Error reading '%s'\n", arg);
Shell::Exit(1);
}
Shell::options.script_executed = true;
if (!Shell::ExecuteString(isolate, source, file_name, false, true)) {
exception_was_thrown = true;
break;
}
ScriptOrigin origin(name);
if (compile_options == ScriptCompiler::kNoCompileOptions) {
ScriptCompiler::Source script_source(source, origin);
return ScriptCompiler::Compile(context, &script_source, compile_options);
}
Which will delegate to ScriptCompiler(Local
auto maybe = CompileUnboundInternal(isolate, source, options);
CompileUnboundInternal
result = i::Compiler::GetSharedFunctionInfoForScript(
str, name_obj, line_offset, column_offset, source->resource_options,
source_map_url, isolate->native_context(), NULL, &script_data, options,
i::NOT_NATIVES_CODE);
src/compiler.cc
// Compile the function and add it to the cache.
ParseInfo parse_info(script);
Zone compile_zone(isolate->allocator(), ZONE_NAME);
CompilationInfo info(&compile_zone, &parse_info, Handle<JSFunction>::null());
Back in src/compiler.cc-info.cc:
result = CompileToplevel(&info);
(lldb) job *result
0x17df0df309f1: [SharedFunctionInfo]
- name = 0x1a7f12d82471 <String[0]: >
- formal_parameter_count = 0
- expected_nof_properties = 10
- ast_node_count = 23
- instance class name = #Object
- code = 0x1d8484d3661 <Code: BUILTIN>
- source code = function bajja(a, b, c) {
var d = c - 100;
return a + d * b;
}
var result = bajja(2, 2, 150);
print(result);
- anonymous expression
- function token position = -1
- start position = 0
- end position = 114
- no debug info
- length = 0
- optimized_code_map = 0x1a7f12d82241 <FixedArray[0]>
- feedback_metadata = 0x17df0df30d09: [FeedbackMetadata]
- length: 3
- slot_count: 11
Slot #0 LOAD_GLOBAL_NOT_INSIDE_TYPEOF_IC
Slot #2 kCreateClosure
Slot #3 LOAD_GLOBAL_NOT_INSIDE_TYPEOF_IC
Slot #5 CALL_IC
Slot #7 CALL_IC
Slot #9 LOAD_GLOBAL_NOT_INSIDE_TYPEOF_IC
- bytecode_array = 0x17df0df30c61
Back in d8.cc:
maybe_result = script->Run(realm);
src/api.cc
auto fun = i::Handle<i::JSFunction>::cast(Utils::OpenHandle(this));
(lldb) job *fun
0x17df0df30e01: [Function]
- map = 0x19cfe0003859 [FastProperties]
- prototype = 0x17df0df043b1
- elements = 0x1a7f12d82241 <FixedArray[0]> [FAST_HOLEY_ELEMENTS]
- initial_map =
- shared_info = 0x17df0df309f1 <SharedFunctionInfo>
- name = 0x1a7f12d82471 <String[0]: >
- formal_parameter_count = 0
- context = 0x17df0df03bf9 <FixedArray[245]>
- feedback vector cell = 0x17df0df30ed1 Cell for 0x17df0df30e49 <FixedArray[13]>
- code = 0x1d8484d3661 <Code: BUILTIN>
- properties = 0x1a7f12d82241 <FixedArray[0]> {
#length: 0x2c35a5718089 <AccessorInfo> (const accessor descriptor)
#name: 0x2c35a57180f9 <AccessorInfo> (const accessor descriptor)
#arguments: 0x2c35a5718169 <AccessorInfo> (const accessor descriptor)
#caller: 0x2c35a57181d9 <AccessorInfo> (const accessor descriptor)
#prototype: 0x2c35a5718249 <AccessorInfo> (const accessor descriptor)
}
i::Handle<i::Object> receiver = isolate->global_proxy();
Local<Value> result;
has_pending_exception = !ToLocal<Value>(i::Execution::Call(isolate, fun, receiver, 0, nullptr), &result);
src/execution.cc
Zone
Taken directly from src/zone/zone.h:
// The Zone supports very fast allocation of small chunks of
// memory. The chunks cannot be deallocated individually, but instead
// the Zone supports deallocating all chunks in one fast
// operation. The Zone is used to hold temporary data structures like
// the abstract syntax tree, which is deallocated after compilation.
V8 flags
$ ./d8 --help
d8
(lldb) br s -f d8.cc -l 2935
return v8::Shell::Main(argc, argv);
api.cc:6112
i::ReadNatives();
natives-external.cc
v8::String::NewFromOneByte
So I was a little confused when I first read this function name and thought it had something to do with the length of the string. But the byte is the type of the chars that make up the string. For example, a one byte char would be reinterpreted as uint8_t:
const char* data
reinterpret_cast<const uint8_t*>(data)
Tasks
- gdbinit has been updated. Check if there is something that should be ported to lldbinit
Invocation walkthrough
This section will go through calling a Script to understand what happens in V8.
I'll be using run-scripts.cc as the example for this.
$ lldb -- ./run-scripts
(lldb) br s -n main
I'll step through until the following call:
script->Run(context).ToLocalChecked();
So, Script::Run is defined in api.cc First things that happens in this function is a macro:
PREPARE_FOR_EXECUTION_WITH_CONTEXT_IN_RUNTIME_CALL_STATS_SCOPE(
"v8",
"V8.Execute",
context,
Script,
Run,
MaybeLocal<Value>(),
InternalEscapableScope,
true);
TRACE_EVENT_CALL_STATS_SCOPED(isolate, category, name);
PREPARE_FOR_EXECUTION_GENERIC(isolate, context, class_name, function_name, \
bailout_value, HandleScopeClass, do_callback);
So, what does the preprocessor replace this with then:
auto isolate = context.IsEmpty() ? i::Isolate::Current() : reinterpret_cast<i::Isolate*>(context->GetIsolate());
I'm skipping TRACE_EVENT_CALL_STATS_SCOPED for now.
PREPARE_FOR_EXECUTION_GENERIC will be replaced with:
if (IsExecutionTerminatingCheck(isolate)) { \
return bailout_value; \
} \
HandleScopeClass handle_scope(isolate); \
CallDepthScope<do_callback> call_depth_scope(isolate, context); \
LOG_API(isolate, class_name, function_name); \
ENTER_V8_DO_NOT_USE(isolate); \
bool has_pending_exception = false
auto fun = i::Handle<i::JSFunction>::cast(Utils::OpenHandle(this));
(lldb) job *fun
0x33826912c021: [Function]
- map = 0x1d0656c03599 [FastProperties]
- prototype = 0x338269102e69
- elements = 0x35190d902241 <FixedArray[0]> [FAST_HOLEY_ELEMENTS]
- initial_map =
- shared_info = 0x33826912bc11 <SharedFunctionInfo>
- name = 0x35190d902471 <String[0]: >
- formal_parameter_count = 0
- context = 0x338269102611 <FixedArray[265]>
- feedback vector cell = 0x33826912c139 <Cell value= 0x33826912c069 <FixedArray[24]>>
- code = 0x1319e25fcf21 <Code BUILTIN>
- properties = 0x35190d902241 <FixedArray[0]> {
#length: 0x2e9d97ce68b1 <AccessorInfo> (const accessor descriptor)
#name: 0x2e9d97ce6921 <AccessorInfo> (const accessor descriptor)
#arguments: 0x2e9d97ce6991 <AccessorInfo> (const accessor descriptor)
#caller: 0x2e9d97ce6a01 <AccessorInfo> (const accessor descriptor)
#prototype: 0x2e9d97ce6a71 <AccessorInfo> (const accessor descriptor)
}
The code for i::JSFunction is generated in src/api.h. Lets take a closer look at this.
#define DECLARE_OPEN_HANDLE(From, To) \
static inline v8::internal::Handle<v8::internal::To> \
OpenHandle(const From* that, bool allow_empty_handle = false);
OPEN_HANDLE_LIST(DECLARE_OPEN_HANDLE)
OPEN_HANDLE_LIST looks like this:
#define OPEN_HANDLE_LIST(V) \
....
V(Script, JSFunction) \
So lets expand this for JSFunction and it should become:
static inline v8::internal::Handle<v8::internal::JSFunction> \
OpenHandle(const Script* that, bool allow_empty_handle = false);
So there will be an function named OpenHandle that will take a const pointer to Script.
A little further down in src/api.h there is another macro which looks like this:
OPEN_HANDLE_LIST(MAKE_OPEN_HANDLE)
MAKE_OPEN_HANDLE:
#define MAKE_OPEN_HANDLE(From, To)
v8::internal::Handle<v8::internal::To> Utils::OpenHandle(
const v8::From* that, bool allow_empty_handle) {
return v8::internal::Handle<v8::internal::To>(
reinterpret_cast<v8::internal::Address*>(const_cast<v8::From*>(that)));
}
And remember that JSFunction is included in the OPEN_HANDLE_LIST so there will
be the following in the source after the preprocessor has processed this header:
A concrete example would look like this:
v8::internal::Handle<v8::internal::JSFunction> Utils::OpenHandle(
const v8::Script* that, bool allow_empty_handle) {
return v8::internal::Handle<v8::internal::JSFunction>(
reinterpret_cast<v8::internal::Address*>(const_cast<v8::Script*>(that))); }
You can inspect the output of the preprocessor using:
$ clang++ -I./out/x64.release/gen -I. -I./include -E src/api/api-inl.h > api-inl.output
So where is JSFunction declared? It is defined in objects.h
Ignition interpreter
User JavaScript also needs to have bytecode generated for them and they also use the C++ DLS and use the CodeStubAssembler -> CodeAssembler -> RawMachineAssembler just like builtins.
C++ Domain Specific Language (DLS)
Build failure
After rebasing I've seen the following issue:
$ ninja -C out/Debug chrome
ninja: Entering directory `out/Debug'
ninja: error: '../../chrome/renderer/resources/plugins/plugin_delay.html', needed by 'gen/chrome/grit/renderer_resources.h', missing and no known rule to make it
The "solution" was to remove the out directory and rebuild.
Tasks
To find suitable task you can use label:HelpWanted at bugs.chromium.org.
OpenHandle
What does this call do:
Utils::OpenHandle(*(source->source_string));
OPEN_HANDLE_LIST(MAKE_OPEN_HANDLE)
Which is a macro defined in src/api.h:
#define MAKE_OPEN_HANDLE(From, To) \
v8::internal::Handle<v8::internal::To> Utils::OpenHandle( \
const v8::From* that, bool allow_empty_handle) { \
DCHECK(allow_empty_handle || that != NULL); \
DCHECK(that == NULL || \
(*reinterpret_cast<v8::internal::Object* const*>(that))->Is##To()); \
return v8::internal::Handle<v8::internal::To>( \
reinterpret_cast<v8::internal::To**>(const_cast<v8::From*>(that))); \
}
OPEN_HANDLE_LIST(MAKE_OPEN_HANDLE)
If we take a closer look at the macro is should expand to something like this in our case:
v8::internal::Handle<v8::internal::To> Utils::OpenHandle(const v8:String* that, false) {
DCHECK(allow_empty_handle || that != NULL); \
DCHECK(that == NULL || \
(*reinterpret_cast<v8::internal::Object* const*>(that))->IsString()); \
return v8::internal::Handle<v8::internal::String>( \
reinterpret_cast<v8::internal::String**>(const_cast<v8::String*>(that))); \
}
So this is returning a new v8::internal::Handle, the constructor is defined in src/handles.h:95.
src/objects.cc
Handle