01、摘要

本篇文章是Builtin专题的第五篇,详细分析Builtin的调用过程。在Bytecode handler中使用CallBuiltin()调用Builtin是最常见的情况,本文将详解CallBuiltin()源码和相关的数据结构。本文内容组织方式:重要数据结构(章节2);CallBuiltin()源码(章节3)。

02、数据结构

提示:Just-In-Time Compiler是本文的前导知识,请读者自行查阅。

Builtin的调用过程主要分为两部分:查询Builtin表找到相应的入口函数、计算calldescriptor。下面解释相关的数据结构:

(1) Builtin名字(例如Builtin::kStoreGlobalIC),名字是枚举类型变量,CallBuiltin()使用该名字查询Builtin表,找到相应的入口函数,源码如下:

class Builtins {
//...............省略....................
  enum Name : int32_t {
#define DEF_ENUM(Name, ...) k##Name,
    BUILTIN_LIST(DEF_ENUM, DEF_ENUM, DEF_ENUM, DEF_ENUM, DEF_ENUM, DEF_ENUM,
                 DEF_ENUM)
#undef DEF_ENUM
        builtin_count,  
}

展开之后如下:

enum Name:int32_t{kRecordWrite, kEphemeronKeyBarrier, kAdaptorWithBuiltinExitFrame, kArgumentsAdaptorTrampoline,......}

(2) Builtin表存储Builtin的地址。Builtin表的位置是isoate->isolatedata->builtins_,源码如下:

class IsolateData final {
 public:
  explicit IsolateData(Isolate* isolate) : stack_guard_(isolate) {}
//............省略....................
  Address* builtins() { return builtins_; }
}

builtins_是Address类型的数组,与enum Name:int32_t{}配合使用可查询对应的Builtin地址(下面的第2行代码就完成了地址查询),源码如下:

1.Callable Builtins::CallableFor(Isolate* isolate, Name name) {
2.  Handle code = isolate->builtins()->builtin_handle(name);
3.  return Callable{code, CallInterfaceDescriptorFor(name)};
4.}

上述代码第3行CallInterfaceDescriptorFor返回Builtin的调用信息,该信息与code共同组成了Callable。

(3) Code类,该类包括Builtin地址、指令的开始和结束以及填充信息,它的作用之一是创建snapshot文件,源码如下:

1.  class Code : public HeapObject {
2.   public:
3.  #define CODE_KIND_LIST(V)   \
4.    V(OPTIMIZED_FUNCTION) V(BYTECODE_HANDLER)       \
5.    V(STUB)  V(BUILTIN)  V(REGEXP)  V(WASM_FUNCTION)   V(WASM_TO_CAPI_FUNCTION)  \
6.     V(WASM_TO_JS_FUNCTION)   V(JS_TO_WASM_FUNCTION)   V(JS_TO_JS_FUNCTION)      \
7.     V(WASM_INTERPRETER_ENTRY)   V(C_WASM_ENTRY)
8.     inline int builtin_index() const;
9.     inline int handler_table_offset() const;
10.     inline void set_handler_table_offset(int offset);
11.     // The body of all code objects has the following layout.
12.     //  +--------------------------+  <-- raw_instruction_start()
13.     //  |       instructions       |
14.     //  |           ...            |
15.     //  +--------------------------+
16.     //  |     embedded metadata    |  <-- safepoint_table_offset()
17.     //  |           ...            |  <-- handler_table_offset()
18.     //  |                          |  <-- constant_pool_offset()
19.     //  |                          |  <-- code_comments_offset()
20.     //  |                          |
21.     //  +--------------------------+  <-- raw_instruction_end()
22.     // If has_unwinding_info() is false, raw_instruction_end() points to the first
23.     // memory location after the end of the code object. Otherwise, the body
24.     // continues as follows:
25.     //  +--------------------------+
26.     //  |    padding to the next   |
27.     //  |  8-byte aligned address  |
28.     //  +--------------------------+  <-- raw_instruction_end()
29.     //  |   [unwinding_info_size]  |
30.     //  |        as uint64_t       |
31.     //  +--------------------------+  <-- unwinding_info_start()
32.     //  |       unwinding info     |
33.     //  |            ...           |
34.     //  +--------------------------+  <-- unwinding_info_end()
35.     // and unwinding_info_end() points to the first memory location after the end
36.     // of the code object.
37.   };

上述代码第3-7行说明了当前Code是哪种指令类型;第9-10代码是异常处理程序;第11-36行注释说明了Code的内存布局。写snapshot文件时内存布局会有细微变化,详情请参考mksnapshot.exe源码。

(4) CallInterfaceDescriptor描述了Builtin入口函数的寄存器参数、堆栈参数和返回值等信息,调用Builtin时会使用这些信息,源码如下:

1.  class V8_EXPORT_PRIVATE CallInterfaceDescriptor {
2.   public:
3.    Flags flags() const { return data()->flags(); }
4.    bool HasContextParameter() const {return (flags() & CallInterfaceDescriptorData::kNoContext) == 0;}
5.    int GetReturnCount() const { return data()->return_count(); }
6.    MachineType GetReturnType(int index) const {return data()->return_type(index);}
7.    int GetParameterCount() const { return data()->param_count(); }
8.    int GetRegisterParameterCount() const {return data()->register_param_count();}
9.    int GetStackParameterCount() const {return data()->param_count() - data()->register_param_count();}
10.    Register GetRegisterParameter(int index) const {return data()->register_param(index);}
11.    MachineType GetParameterType(int index) const {return data()->param_type(index);}
12.    RegList allocatable_registers() const {return data()->allocatable_registers();}
13.  //..............省略...................
14.  private:
15.  const CallInterfaceDescriptorData* data_;
16.  }

上述代码第5行是Builtin的返回值数量;第6行是返回值的类型;第7行是参数数量;第8代是寄存器参数的数量;第9行是栈参数的数量;第10-12行是获取参数;第15行代码CallInterfaceDescriptorData存储上述代码中所需的信息,即返回值数量、类型等信息,源码如下:

1.  class V8_EXPORT_PRIVATE CallInterfaceDescriptorData {
2.   private:
3.    bool IsInitializedPlatformSpecific() const {
4.  //.........省略..............
5.    }
6.    bool IsInitializedPlatformIndependent() const {
7.  //.........省略..............
8.    }
9.    int register_param_count_ = -1;
10.    int return_count_ = -1;
11.    int param_count_ = -1;
12.    Flags flags_ = kNoFlags;
13.    RegList allocatable_registers_ = 0;
14.    Register* register_params_ = nullptr;
15.    MachineType* machine_types_ = nullptr;
16.    DISALLOW_COPY_AND_ASSIGN(CallInterfaceDescriptorData);
17.  };

上述代码第9-15行定义的变量就是CallInterfaceDescriptor中提到的返回值、参数等信息。

以上内容是CallBuiltin()使用的主要数据结构。

03、CallBuiltin()

来看下面的使用场景:

IGNITION_HANDLER(LdaNamedPropertyNoFeedback, InterpreterAssembler) {
  TNode<Object> object = LoadRegisterAtOperandIndex(0);
  TNode<Name> name = CAST(LoadConstantPoolEntryAtOperandIndex(1));
  TNode<Context> context = GetContext();
  TNode<Object> result = CallBuiltin(Builtins::kGetProperty, context, object, name);
  SetAccumulator(result);
  Dispatch();
}

LdaNamedPropertyNoFeedback的作用是获取属性,例如从document属性中获取getelementbyID方法,该方法的获取由CallBuiltin调用Builtins::kGetProperty实现,源码如下:

  template <class... TArgs>
  TNode CallBuiltin(Builtins::Name id, SloppyTNode context,                            TArgs... args) {
    return CallStub(Builtins::CallableFor(isolate(), id), context,                            args...);
  }
上述代码中id代表Builtin的名字,即前面提到的枚举值;args有两个成员:args[0]代表object(上述例子中的document),args[1]代表name(getelementbyID方法)。CallStub()源码如下:
1.    template <class T = Object, class... TArgs>
2.    TNode<T> CallStub(Callable const& callable, SloppyTNode<Object> context,
3.                      TArgs... args) {
4.      TNode<Code> target = HeapConstant(callable.code());
5.      return CallStub<T>(callable.descriptor(), target, context, args...);
6.    }
7.  //..............分隔线..................
8.    template <class T = Object, class... TArgs>
9.    TNode<T> CallStub(const CallInterfaceDescriptor& descriptor,
10.                      SloppyTNode<Code> target, SloppyTNode<Object> context,
11.                      TArgs... args) {
12.      return UncheckedCast<T>(CallStubR(StubCallMode::kCallCodeObject, descriptor,
13.                                        1, target, context, args...));
14.    }
15.  //..............分隔线..................
16.    template <class... TArgs>
17.    Node* CallStubR(StubCallMode call_mode,
18.                    const CallInterfaceDescriptor& descriptor, size_t result_size,
19.                    SloppyTNode<Object> target, SloppyTNode<Object> context,
20.                    TArgs... args) {
21.      return CallStubRImpl(call_mode, descriptor, result_size, target, context,
22.                           {args...});
23.    }
上述代码第4行创建target对象,该对象是Builtin的入口地址;第5行代码调用CallStub()方法(第9行),最终进入CallStubR()。在CallStubR()中调用CallStubRImpl(),源码如下:
1.  Node* CodeAssembler::CallStubRImpl( ) {
2.    DCHECK(call_mode == StubCallMode::kCallCodeObject ||
3.           call_mode == StubCallMode::kCallBuiltinPointer);
4.    constexpr size_t kMaxNumArgs = 10;
5.    DCHECK_GE(kMaxNumArgs, args.size());
6.     NodeArray<kMaxNumArgs + 2> inputs;
7.     inputs.Add(target);
8.     for (auto arg : args) inputs.Add(arg);
9.     if (descriptor.HasContextParameter()) {
10.       inputs.Add(context);
11.     }
12.     return CallStubN(call_mode, descriptor, result_size, inputs.size(),
13.                      inputs.data());
14.   }
上述代码第7-10行将所有参数添加到数组inputs中,inputs内容依次为:Builtin的入口地址(code类型)、object、name、context。进入第12行代码,CallStubN()源码如下:
1.  Node* CodeAssembler::CallStubN() {2.    // implicit nodes are target and optionally context.3.    int implicit_nodes = descriptor.HasContextParameter() ? 2 : 1;4.    int argc = input_count - implicit_nodes;5.    // Extra arguments not mentioned in the descriptor are passed on the stack.6.    int stack_parameter_count = argc - descriptor.GetRegisterParameterCount();7.    auto call_descriptor = Linkage::GetStubCallDescriptor(8.        zone(), descriptor, stack_parameter_count, CallDescriptor::kNoFlags,9.        Operator::kNoProperties, call_mode);10.    CallPrologue();11.    Node* return_value =12.        raw_assembler()->CallN(call_descriptor, input_count, inputs);13.    HandleException(return_value);14.    CallEpilogue();15.    return return_value;16.  }
上述第7行代码call_descriptor的返回值类型如下:
CallDescriptor(          // --
      kind,                                  // kind
      target_type,                           // target MachineType
      target_loc,                            // target location
      locations.Build(),                     // location_sig
      stack_parameter_count,                 // stack_parameter_count
      properties,                            // properties
      kNoCalleeSaved,                        // callee-saved registers
      kNoCalleeSaved,                        // callee-saved fp
      CallDescriptor::kCanUseRoots | flags,  // flags
      descriptor.DebugName(),                // debug name
      descriptor.allocatable_registers())
上述信息为调用Builtin做准备工作。CallStubN()中第11行代码:完成Builtin的调用,第13行代码:异常处理。图1给出了CodeAssembler()的调用堆栈,此时正在建立Builtin,Builtin的建立发生在Isolate初始化阶段。

技术总结

(1) Builtin名字与Builtin的入口之间存在一一对应的关系,这种关系由isoate->isolatedata->builtins表示,builtins是在Isolate初始化过程中创建的Address数组;

(2) inputs数组除了包括参数之外还有target和context;

(3) Builtin的参数、返回值等信息的详细说明可在BUILTIN_LIST宏中查看。

好了,今天到这里,下次见。