Guide for porting V8 classes from legacy Torque layout to the new C++ HeapObjectLayout. Use when asked to move objects, struct subclasses, or field definitions from Torque to C++ as real members.
This skill guides you through porting an arbitrary V8 class from a legacy Torque layout to the new C++ HeapObjectLayout. The core idea is to shift layout authority from Torque generation to explicit C++ definitions using specialized layout primitives (like TaggedMember), while keeping Torque informed so it can verify the layout and use it in CodeStubAssembler (CSA) and builtins.
Crucial Constraint: A C++ layout object (HeapObjectLayout subclass) cannot inherit from a legacy Torque layout object, and vice versa. Because of this, migrations must be done for entire object inheritance (sub)trees at a time.
If you are migrating a base class, you must generally migrate all of its subclasses in the same operation. To make large inheritance trees manageable, you can subdivide the tree into smaller subtrees by introducing intermediate C++ layout base classes (e.g., StructLayout, PrimitiveHeapObject). Once an intermediate base class is migrated (and subclasses updated to inherit from it or its Torque equivalent temporarily), its subclasses can be grouped and migrated in more manageable batches.
.tq)Torque needs to know that the layout is now managed by C++, but it still needs the field definitions to generate CSA/Builtin offsets and verification assertions.
.tq file.extern class definition, add the @cppObjectLayoutDefinition annotation.extern: Ensure the class is declared as extern.// Example: src/objects/my-object.tq
@cppObjectLayoutDefinition
extern class MyObject extends Struct {
// KEEP these fields here! Torque needs them for layout verification.
flags: SmiTagged<MyObjectFlags>;
value: JSAny|TheHole;
smi_value: Smi;
weak_ref: Weak<Map>|Undefined;
}
.h)Define the explicit memory layout in the C++ header.
#include "src/objects/object-macros.h".V8_OBJECT and V8_OBJECT_END.StructLayout, HeapObjectLayout, TrustedObjectLayout)._ to their names. Use TaggedMember combined with UnionOf or Weak where appropriate:
TaggedMember<UnionOf<JSAny, Hole>> value_; (Maps to JSAny|TheHole)TaggedMember<UnionOf<Weak<Map>, Undefined>> weak_ref_;TaggedMember<Smi> flags_;UnalignedDoubleMember float_field_;ExternalPointerMember<kMyTag> ptr_;JSDispatchHandle, strongly-typed enums) rather than raw low-level types (like int32_t or uint32_t) whenever conceptually appropriate.public section. Return types should match the UnionOf types exactly.
UnionOf types, use a public typedef (e.g., using Value = UnionOf<...>;) within the class to improve readability of accessors and field declarations.PtrComprCageBase getter overloads (e.g. value(cage_base)) as TaggedMember handles decompression natively.DECL_PRINTER(MyObject) and DECL_VERIFIER(MyObject).kAlignedSize), define them as inline constexpr int in the header, outside the V8_OBJECT block to avoid duplicate symbol errors during linking.// Example: src/objects/my-object.h
#include "src/objects/struct.h"
#include "src/objects/object-macros.h" // Must be the last include
namespace v8::internal {
#include "torque-generated/src/objects/my-object-tq.inc"
V8_OBJECT class MyObject : public StructLayout {
public:
// Accessors
inline uint32_t flags() const;
inline void set_flags(uint32_t value);
inline Tagged<UnionOf<JSAny, Hole>> value() const;
inline void set_value(Tagged<UnionOf<JSAny, Hole>> value, WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
inline int smi_value() const;
inline void set_smi_value(int value);
inline Tagged<UnionOf<Weak<Map>, Undefined>> weak_ref() const;
inline void set_weak_ref(Tagged<UnionOf<Weak<Map>, Undefined>> value, WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
// GC Body Descriptor
using BodyDescriptor = StructBodyDescriptor;
// Diagnostics
DECL_PRINTER(MyObject)
DECL_VERIFIER(MyObject)
// Fields (Public for simplified access and Torque asserts)
TaggedMember<Smi> flags_;
TaggedMember<UnionOf<JSAny, Hole>> value_;
TaggedMember<UnionOf<Weak<Map>, Undefined>> weak_ref_;
} V8_OBJECT_END;
} // namespace v8::internal
#include "src/objects/object-macros-undef.h"
-inl.h)Implement the accessors using the TaggedMember APIs.
// Example: src/objects/my-object-inl.h
#include "src/objects/my-object.h"
#include "src/objects/objects-inl.h"
#include "src/objects/object-macros.h"
namespace v8::internal {
#include "torque-generated/src/objects/my-object-tq-inl.inc"
int MyObject::flags() const {
return flags_.load().value();
}
void MyObject::set_flags(int value) {
flags_.store(this, Smi::FromInt(value));
}
Tagged<UnionOf<JSAny, Hole>> MyObject::value() const {
return value_.load();
}
void MyObject::set_value(Tagged<UnionOf<JSAny, Hole>> value, WriteBarrierMode mode) {
value_.store(this, value, mode);
}
Tagged<UnionOf<Weak<Map>, Undefined>> MyObject::weak_ref() const {
return weak_ref_.load();
}
void MyObject::set_weak_ref(Tagged<UnionOf<Weak<Map>, Undefined>> value, WriteBarrierMode mode) {
weak_ref_.store(this, value, mode);
}
} // namespace v8::internal
#include "src/objects/object-macros-undef.h"
this to Write BarriersIf you need to pass the current object to a write barrier macro or function (like CONDITIONAL_WRITE_BARRIER or JS_DISPATCH_HANDLE_WRITE_BARRIER), it might currently expect a Tagged<HeapObject>.
Do not cast this to a Tagged pointer (e.g., Tagged<MyObject>(this)). Instead, follow the same overloading advice as with internal APIs: add an overload to the underlying write barrier function (e.g., WriteBarrier::ForJSDispatchHandle) so that it natively accepts your layout object pointer.
// Incorrect (Casting `this`):
JS_DISPATCH_HANDLE_WRITE_BARRIER(Tagged<MyObject>(this), new_handle);
JS_DISPATCH_HANDLE_WRITE_BARRIER(Tagged<HeapObject>(ptr()), new_handle);
// Correct (Add an overload if necessary, then pass `this` directly):
JS_DISPATCH_HANDLE_WRITE_BARRIER(this, new_handle);
If your class previously used atomic macros like DECL_RELAXED_ACCESSORS or DECL_ACQUIRE_GETTER, you can port these directly to TaggedMember which provides built-in support for atomic memory orderings:
field_.Acquire_Load()field_.Release_Store(this, value, mode)field_.Relaxed_Load()field_.Relaxed_Store(this, value, mode)// In the .h file
inline Tagged<Object> my_atomic_field(AcquireLoadTag) const;
inline void set_my_atomic_field(Tagged<Object> value, ReleaseStoreTag,
WriteBarrierMode mode = UPDATE_WRITE_BARRIER);
// In the -inl.h file
Tagged<Object> MyObject::my_atomic_field(AcquireLoadTag) const {
return my_atomic_field_.Acquire_Load();
}
void MyObject::set_my_atomic_field(Tagged<Object> value, ReleaseStoreTag,
WriteBarrierMode mode) {
my_atomic_field_.Release_Store(this, value, mode);
}
Legacy classes often used RELAXED_READ_BYTE_FIELD(obj, kFooOffset) /
RELAXED_WRITE_UINT16_FIELD(...) / ACQUIRE_READ_UINT32_FIELD(...) style
macros to access primitive uint8_t / uint16_t / uint32_t fields
atomically. These macros expand to FIELD_ADDR(obj, offset) which does
not exist on HeapObjectLayout subclasses.
Do not add RELAXED_READ_BYTE(&obj->field_) style "field-pointer"
macros as a workaround. Instead, declare the field as a std::atomic
member of the appropriate width and use the standard .load() /
.store() accessors with std::memory_order_*:
// In the V8_OBJECT body:
FLEXIBLE_ARRAY_MEMBER(ElementT, name) after the fixed members. Per-index access is then natural via the generated name() accessor combined with TaggedMember's atomic methods, avoiding any need to reach for the static TaggedField<> API. For tagged elements use FLEXIBLE_ARRAY_MEMBER(TaggedMember<JSAny>, objects); for raw embedder slots use FLEXIBLE_ARRAY_MEMBER(Address, slots). Example accessors:
Tagged<JSAny> MyArray::get(int index) const {
return objects()[index].Relaxed_Load();
}
void MyArray::set(int index, Tagged<JSAny> value, WriteBarrierMode mode) {
objects()[index].Relaxed_Store(this, value, mode);
}
Tagged<JSAny> MyArray::Swap(int index, Tagged<JSAny> value, SeqCstAccessTag) {
return objects()[index].SeqCst_Swap(this, value);
}
SizeFor(int length) and OffsetOfElementAt(int index) should be declared inside the class but defined out of line so they can use OFFSET_OF_DATA_START(MyObject) (which references the FAM and is therefore not visible mid-class-body):
V8_OBJECT class MyObject : public HeapObjectLayout {
// ...
static constexpr int SizeFor(int length);
// ...
FLEXIBLE_ARRAY_MEMBER(TaggedMember<JSAny>, objects);
} V8_OBJECT_END;
constexpr int MyObject::SizeFor(int length) {
return OFFSET_OF_DATA_START(MyObject) + length * kTaggedSize;
}
The BodyDescriptor typedef can also use OFFSET_OF_DATA_START:
template <>
struct ObjectTraits<MyObject> {
using BodyDescriptor =
FlexibleBodyDescriptor<OFFSET_OF_DATA_START(MyObject)>;
};