Understanding Chrome V8 — Chapter 14: What is Dynamically Typed JS

Written by huidou | Published 2022/09/17
Tech Story Tags: javascript | nodejs | google-chrome | javascript-development | chrome | understanding-chrome-v8 | javascript-fundamentals | understanding-javascript

TLDRIn C++, a JavaScript object is just a piece of memory with no type. C++ doesn’t know how many and what values are in this memory. In JavaScript, these values can change dynamically, so how can C++ know the latest type in order to maintain a JavaScript. V8 updates the latest addressing method into the JavasCript object’s map in time once its type changes. In this way, JavaScript developers can alter the object type freely. The technology behind the dynamic features is powered by C++.via the TL;DR App

Welcome to other chapters of Let’s Understand Chrome V8.

JavaScript is a dynamically typed language, so we don’t care about the data type, just use var to define the variable we want. But in C++, you have to figure out and read up a lot about integers and longs and chars and so on to just a very simple hello world program. It’s C++ which is a statically typed language. So, how does a static language like C++ power the dynamically typed JavaScript? That’s what I would like to talk about in this article.

1. Map layout

In C++, a JavaScript object is just a piece of memory with no type. C++ doesn’t know how many and what values are in this memory. In JavaScript philosophy, these values can change dynamically, so how can C++ know the latest type in order to maintain a JavaScript object?

Look at Figure 1, let’s talk about the map class used by V8, namely the hidden class.

  • From a JavaScript developer’s perspective, you can only see blue memory, which is your JavaScript object;
  • In V8, I can see all memory, green memory (map), and blue memory (JavaScript objects).

The map size is 80bytes, and it is used to protect the type of a JavaScript object, which is the layout of the blue memory — what data is stored, strings, arrays, or other? How to read this data? Namely, the addressing method.

Note: Map only saves the addressing method, not the data in a JavaScript object.

Before maintaining a JavaScript object, V8 takes out the addressing method from the map first, then can read and write blue memory correctly. V8 updates the latest addressing method into the JavasCript object’s map in time once its type changes. In this way, JavaScript developers can alter the object type freely.

To sum up, in C++, the data type is still static, and the most important thing is to update the map in time when the type changes. It is the truth that you’d like to know. The technology behind the dynamic features is powered by C++.

The description of the map is below.

1.  Map layout:
2.  // +---------------+---------------------------------------------+
3.  // |   _ Type _    | _ Description _                             |
4.  // +---------------+---------------------------------------------+
5.  // | TaggedPointer | map - Always a pointer to the MetaMap root  |
6.  // +---------------+---------------------------------------------+
7.  // | Int           | The first int field                         |
8.  //  `---+----------+---------------------------------------------+
9.  //      | Byte     | [instance_size]                             |
10.  //      +----------+---------------------------------------------+
11.  //      | Byte     | If Map for a primitive type:                |
12.  //      |          |   native context index for constructor fn   |
13.  //      |          | If Map for an Object type:                  |
14.  //      |          |   inobject properties start offset in words |
15.  //      +----------+---------------------------------------------+
16.  //      | Byte     | [used_or_unused_instance_size_in_words]     |
17.  //      |          | For JSObject in fast mode this byte encodes |
18.  //      |          | the size of the object that includes only   |
19.  //      |          | the used property fields or the slack size  |
20.  //      |          | in properties backing store.                |
21.  //      +----------+---------------------------------------------+
22.  //      | Byte     | [visitor_id]                                |
23.  // +----+----------+---------------------------------------------+
24.  // | Int           | The second int field                        |
25.  //  `---+----------+---------------------------------------------+
26.  //      | Short    | [instance_type]                             |
27.  //      +----------+---------------------------------------------+
28.  //      | Byte     | [bit_field]                                 |
29.  //      |          |   - has_non_instance_prototype (bit 0)      |
30.  //      |          |   - is_callable (bit 1)                     |
31.  //      |          |   - has_named_interceptor (bit 2)           |
32.  //      |          |   - has_indexed_interceptor (bit 3)         |
33.  //      |          |   - is_undetectable (bit 4)                 |
34.  //      |          |   - is_access_check_needed (bit 5)          |
35.  //      |          |   - is_constructor (bit 6)                  |
36.  //      |          |   - has_prototype_slot (bit 7)              |
37.  //      +----------+---------------------------------------------+
38.  //      | Byte     | [bit_field2]                                |
39.  //      |          |   - new_target_is_base (bit 0)              |
40.  //      |          |   - is_immutable_proto (bit 1)              |
41.  //      |          |   - unused bit (bit 2)                      |
42.  //      |          |   - elements_kind (bits 3..7)               |
43.  // +----+----------+---------------------------------------------+
44.  // | Int           | [bit_field3]                                |
45.  // |               |   - enum_length (bit 0..9)                  |
46.  // |               |   - number_of_own_descriptors (bit 10..19)  |
47.  // |               |   - is_prototype_map (bit 20)               |
48.  // |               |   - is_dictionary_map (bit 21)              |
49.  // |               |   - owns_descriptors (bit 22)               |
50.  // |               |   - is_in_retained_map_list (bit 23)        |
51.  // |               |   - is_deprecated (bit 24)                  |
52.  // |               |   - is_unstable (bit 25)                    |
53.  // |               |   - is_migration_target (bit 26)            |
54.  // |               |   - is_extensible (bit 28)                  |
55.  // |               |   - may_have_interesting_symbols (bit 28)   |
56.  // |               |   - construction_counter (bit 29..31)       |
57.  // |               |                                             |
58.  // +*************************************************************+
59.  // | Int           | On systems with 64bit pointer types, there  |
60.  // |               | is an unused 32bits after bit_field3        |
61.  // +*************************************************************+
62.  // | TaggedPointer | [prototype]                                 |
63.  // +---------------+---------------------------------------------+
64.  // | TaggedPointer | [constructor_or_backpointer]                |
65.  // +---------------+---------------------------------------------+
66.  // | TaggedPointer | [instance_descriptors]                      |
67.  // +*************************************************************+
68.  // ! TaggedPointer ! [layout_descriptors]                        !
69.  // !               ! Field is only present if compile-time flag  !
70.  // !               ! FLAG_unbox_double_fields is enabled         !
71.  // !               ! (basically on 64 bit architectures)         !
72.  // +*************************************************************+
73.  // | TaggedPointer | [dependent_code]                            |
74.  // +---------------+---------------------------------------------+
75.  // | TaggedPointer | [prototype_validity_cell]                   |
76.  // +---------------+---------------------------------------------+
77.  // | TaggedPointer | If Map is a prototype map:                  |
78.  // |               |   [prototype_info]                          |
79.  // |               | Else:                                       |
80.  // |               |   [raw_transitions]                         |
81.  // +---------------+---------------------------------------------+

The size and layout of the map are predefined constants that do not change. Each heap-managed object has a map. The shape of both two objects is the same, in other words, the two objects have the same internal members and member positions which means that they have the same addressing method, so they share the same map.

See the code below.

function Point(x,y) {
	this.x = x;
	this.y = y;
}

var fun1 = new Point(1,2);
var fun2 = new Point(3,4);
fun2.z = 80;

The fun1 and fun2 come from the same constructor, they have the same shape, so they share the same map. But after fun2.z=80, the shape of fun2 is changed, so V8 assigns a new map for it.

The class map is below.

1.  class Map : public HeapObject {
2.   public:
3.  //.............omit..................
4.     DECL_PRIMITIVE_ACCESSORS(bit_field, byte)
5.     DECL_PRIMITIVE_ACCESSORS(relaxed_bit_field, byte)
6.   // Bit positions for |bit_field|.
7.   #define MAP_BIT_FIELD_FIELDS(V, _)          \
8.     V(HasNonInstancePrototypeBit, bool, 1, _) \
9.     V(IsCallableBit, bool, 1, _)              \
10.     V(HasNamedInterceptorBit, bool, 1, _)     \
11.     V(HasIndexedInterceptorBit, bool, 1, _)   \
12.     V(IsUndetectableBit, bool, 1, _)          \
13.     V(IsAccessCheckNeededBit, bool, 1, _)     \
14.     V(IsConstructorBit, bool, 1, _)           \
15.     V(HasPrototypeSlotBit, bool, 1, _)
16.     DEFINE_BIT_FIELDS(MAP_BIT_FIELD_FIELDS)
17.   #undef MAP_BIT_FIELD_FIELDS
18.     // Bit field 2.
19.     DECL_PRIMITIVE_ACCESSORS(bit_field2, byte)
20.   // Bit positions for |bit_field2|.
21.   #define MAP_BIT_FIELD2_FIELDS(V, _)      \
22.     V(NewTargetIsBaseBit, bool, 1, _)      \
23.     V(IsImmutablePrototypeBit, bool, 1, _) \
24.     V(UnusedBit, bool, 1, _)               \
25.     V(ElementsKindBits, ElementsKind, 5, _)
26.     DEFINE_BIT_FIELDS(MAP_BIT_FIELD2_FIELDS)
27.   #undef MAP_BIT_FIELD2_FIELDS
28.     DECL_PRIMITIVE_ACCESSORS(bit_field3, uint32_t)
29.     V8_INLINE void clear_padding();
30.      DEFINE_FIELD_OFFSET_CONSTANTS(HeapObject::kHeaderSize,
31.                                    TORQUE_GENERATED_MAP_FIELDS)
32.      //.............omit..................
33.      OBJECT_CONSTRUCTORS(Map, HeapObject);
34.  };

Below is the expansion of the DEFINE_FIELD_OFFSET_CONSTANTS macro.

1.    enum {
2.  TORQUE_GENERATED_MAP_FIELDS_StartOffset= 7,
3.  kInstanceSizeInWordsOffset=8, kInstanceSizeInWordsOffsetEnd = 8,
4.  kInObjectPropertiesStartOrConstructorFunctionIndexOffset=9, kInObjectPropertiesStartOrConstructorFunctionIndexOffsetEnd = 9,
5.  kUsedOrUnusedInstanceSizeInWordsOffset=10, kUsedOrUnusedInstanceSizeInWordsOffsetEnd = 10,
6.  kVisitorIdOffset=11, kVisitorIdOffsetEnd = 11,
7.  kInstanceTypeOffset=12, kInstanceTypeOffsetEnd = 13,
8.  kBitFieldOffset=14, kBitFieldOffsetEnd = 14,
9.  kBitField2Offset=15, kBitField2OffsetEnd = 15,
10.  kBitField3Offset=16, kBitField3OffsetEnd = 19,
11.  kOptionalPaddingOffset=20, kOptionalPaddingOffsetEnd = 23,
12.  kStartOfStrongFieldsOffset=24, kStartOfStrongFieldsOffsetEnd = 23,
13.  kPrototypeOffset=24, kPrototypeOffsetEnd = 31,
14.  kConstructorOrBackPointerOffset=32, kConstructorOrBackPointerOffsetEnd = 39,
15.  kInstanceDescriptorsOffset=40, kInstanceDescriptorsOffsetEnd = 47,
16.  kLayoutDescriptorOffset=48, kLayoutDescriptorOffsetEnd = 55,
17.  kDependentCodeOffset=56, kDependentCodeOffsetEnd = 63,
18.  kPrototypeValidityCellOffset=64, kPrototypeValidityCellOffsetEnd = 71,
19.  kEndOfStrongFieldsOffset=72, kEndOfStrongFieldsOffsetEnd = 71,
20.  kStartOfWeakFieldsOffset=72, kStartOfWeakFieldsOffsetEnd = 71,
21.  kTransitionsOrPrototypeInfoOffset=72, kTransitionsOrPrototypeInfoOffsetEnd = 79,
22.  kEndOfWeakFieldsOffset=80, kEndOfWeakFieldsOffsetEnd = 79,
23.  kSize=80, kSizeEnd = 79,
24.    }

The enumeration here is consistent with the map description mentioned above. Note that, in the new version of V8 in the future, the map may change, and new member variables may appear in this enumeration.

A map is a heap-managed object of V8. When new a map, V8 uses the following function to allocate memory from the V8 heap.

1.  AllocationResult Heap::AllocateRaw(int size_in_bytes, AllocationType type,
2.                                     AllocationOrigin origin,
3.                                     AllocationAlignment alignment) {
4.  //.....omit.......
5.    if (AllocationType::kYoung == type) {
6.  //.....omit.......
7.    } else if (AllocationType::kOld == type) {
8.  //.....omit.......
9.    } else if (AllocationType::kCode == type) {
10.      if (size_in_bytes <= code_space()->AreaSize() && !large_object) {
11.        allocation = code_space_->AllocateRawUnaligned(size_in_bytes);
12.      } else {
13.        allocation = code_lo_space_->AllocateRaw(size_in_bytes);
14.      }
15.    } else if (AllocationType::kMap == type) {
16.      allocation = map_space_->AllocateRawUnaligned(size_in_bytes);
17.    } else if (AllocationType::kReadOnly == type) {
18.  #ifdef V8_USE_SNAPSHOT
19.      DCHECK(isolate_->serializer_enabled());
20.  #endif
21.      DCHECK(!large_object);
22.      DCHECK(CanAllocateInReadOnlySpace());
23.      DCHECK_EQ(AllocationOrigin::kRuntime, origin);
24.      allocation =
25.          read_only_space_->AllocateRaw(size_in_bytes, alignment, origin);
26.    } else {
27.      UNREACHABLE();
28.    }
29.    return allocation;
30.  }

When line 15 is true, the type is kMap and size_in_bytes is 80, which means that is allocating memory for a new map. Figure 2 is the call stack.

2. Map initialization

During the V8 startup, the below CreateInitialMaps initialize empty maps for all JavaScript types. The empty map descript the minimum requirements for a JavaScript object. When you have new a JavaScript object, V8 fills some necessary information (object type, size …) into the corresponding empty map.

1.  bool Heap::CreateInitialMaps() {
2.    HeapObject obj;
3.    {
4.      AllocationResult allocation = AllocatePartialMap(MAP_TYPE, Map::kSize);
5.      if (!allocation.To(&obj)) return false;
6.    }
7.    Map new_meta_map = Map::unchecked_cast(obj);
8.    set_meta_map(new_meta_map);
9.    new_meta_map.set_map_after_allocation(new_meta_map);
10.  //...................omit...................
11.    ReadOnlyRoots roots(this);
12.    {  // Partial map allocation
13.  #define ALLOCATE_PARTIAL_MAP(instance_type, size, field_name)                \
14.    {                                                                          \
15.      Map map;                                                                 \
16.      if (!AllocatePartialMap((instance_type), (size)).To(&map)) return false; \
17.      set_##field_name##_map(map);                                             \
18.    }
19.      ALLOCATE_PARTIAL_MAP(FIXED_ARRAY_TYPE, kVariableSizeSentinel, fixed_array);
20.      ALLOCATE_PARTIAL_MAP(WEAK_FIXED_ARRAY_TYPE, kVariableSizeSentinel,
21.                           weak_fixed_array);
22.      ALLOCATE_PARTIAL_MAP(WEAK_ARRAY_LIST_TYPE, kVariableSizeSentinel,
23.  //..................omit...................
24.  #undef ALLOCATE_PARTIAL_MAP
25.    }
26.    // Allocate the empty array.
27.    {
28.      AllocationResult alloc =
29.          AllocateRaw(FixedArray::SizeFor(0), AllocationType::kReadOnly);
30.      if (!alloc.To(&obj)) return false;
31.      obj.set_map_after_allocation(roots.fixed_array_map(), SKIP_WRITE_BARRIER);
32.      FixedArray::cast(obj).set_length(0);
33.    }
34.    set_empty_fixed_array(FixedArray::cast(obj));
35.  //...................omit....................
36.    FinalizePartialMap(roots.meta_map());
37.    FinalizePartialMap(roots.fixed_array_map());
38.    FinalizePartialMap(roots.weak_fixed_array_map());
39.    {
40.      if (!AllocateRaw(FixedArray::SizeFor(0), AllocationType::kReadOnly)
41.               .To(&obj)) {
42.        return false;
43.      }
44.      obj.set_map_after_allocation(roots.closure_feedback_cell_array_map(),
45.                                   SKIP_WRITE_BARRIER);
46.      FixedArray::cast(obj).set_length(0);
47.      set_empty_closure_feedback_cell_array(ClosureFeedbackCellArray::cast(obj));
48.    }
49.    DCHECK(!InYoungGeneration(roots.empty_fixed_array()));
50.    roots.bigint_map().SetConstructorFunctionIndex(
51.        Context::BIGINT_FUNCTION_INDEX);
52.    return true;
53.  }

Lines 4, 5, 6, 7, and 8 create meta_data for all maps. From lines 13 to 22, use the macro ALLOCATE_PARTIAL_MAP to create empty maps for ARRAY_LIST and ARRAY. From lines 27 to 34, create empty maps for other types. The map creations are in batches because the subsequent creation needs to use the previous results.

On line 36, all empty maps are saved to roots_table, as shown in Figure 3.

Root_table is defined by the following macro.

#define READ_ONLY_ROOT_LIST(V)     \
  STRONG_READ_ONLY_ROOT_LIST(V)    \
  INTERNALIZED_STRING_ROOT_LIST(V) \
  PRIVATE_SYMBOL_ROOT_LIST(V)      \
  PUBLIC_SYMBOL_ROOT_LIST(V)       \
  WELL_KNOWN_SYMBOL_ROOT_LIST(V)   \
  STRUCT_MAPS_LIST(V)              \
  ALLOCATION_SITE_MAPS_LIST(V)     \
  DATA_HANDLER_MAPS_LIST(V)

#define MUTABLE_ROOT_LIST(V)                \
  STRONG_MUTABLE_IMMOVABLE_ROOT_LIST(V)     \
  STRONG_MUTABLE_MOVABLE_ROOT_LIST(V)       \
  V(StringTable, string_table, StringTable) \
  SMI_ROOT_LIST(V)

#define ROOT_LIST(V)     \
  READ_ONLY_ROOT_LIST(V) \
  MUTABLE_ROOT_LIST(V)

Root_table holds not only the map but also other heap objects. Through the parameters of the macro template, we can roughly guess the role of each element. If you want to dive deeper, please debug.

Okay, that wraps it up for this share. I’ll see you guys next time, take care!

Please reach out to me if you have any issues.

WeChat: qq9123013 Email: [email protected]


Also published here.


Written by huidou | a big fan of chrome V8
Published by HackerNoon on 2022/09/17