通过Handle理解V8的代码设计(基于V0.1.5)

开发 前端
Handle在V8里是一个非常重要的概念,本文从早期的源码分析Handle的原理,在分析的过程中我们还可以看到V8在代码设计上的一些细节。

[[402071]]

本文转载自微信公众号「编程杂技」,作者theanarkh。转载本文请联系编程杂技公众号。

前言:Handle在V8里是一个非常重要的概念,本文从早期的源码分析Handle的原理,在分析的过程中我们还可以看到V8在代码设计上的一些细节。

假设我们有以下代码

  1. HandleScope scope; 
  2. Local<String> hello = String::New(参数); 

这个看起来很简单的过程,其实在V8的内部实现起来比较复杂。

HandleScope

我们从创建一个HandleScope对象开始分析。HandleScope是负责管理多个Handle的对象,主要是为了方便管理Handle的分配和释放。

  1. class HandleScope { 
  2.  public
  3.   HandleScope() : previous_(current_), is_closed_(false) { 
  4.     current_.extensions = 0; 
  5.   } 
  6.  
  7.   static void** CreateHandle(void* value); 
  8.  
  9.  private: 
  10.  
  11.   class Data { 
  12.    public
  13.     // 分配了一块内存后,又额外分配的块数 
  14.     int extensions; 
  15.     // 下一个可用的位置 
  16.     void** next
  17.     // 达到limit执行的地址后说明当前内存块用完了 
  18.     void** limit; 
  19.     inline void Initialize() { 
  20.       extensions = -1; 
  21.       next = limit = NULL
  22.     } 
  23.   }; 
  24.   // 当前的HandleScope 
  25.   static Data current_; 
  26.   // 上一个HandleScope 
  27.   const Data previous_; 
  28. }; 
  29.  
  30. HandleScope::Data HandleScope::current_ = { -1, NULLNULL }; 

通过HandleScope的构造函数我们知道每次定义一个HandleScope对象的时候,previous就会指向前一个HandleScope的数据(但是current_除了第一次创建HandleScope的时候更新了(见CreateHandle),后续似乎没有更新?后续详细看一下),从HandleScope的定义中我们知道他的布局如下。

接着我们看HandleScope的CreateHandle方法。

  1. void** v8::HandleScope::CreateHandle(void* value) { 
  2.   // 获取下一个可用的地址 
  3.   void** result = current_.next
  4.   // 到达limit的地址了或者为空(初始化的时候)则获取新的内存 
  5.   if (result == current_.limit) { 
  6.     // Block是二维数组,每个元素指向一个可以存储数据的数组。非空说明可能有可用的内存空间 
  7.     if (!thread_local.Blocks()->is_empty()) { 
  8.       // 拿到list中最后一个元素,得到一个数组首地址,然后再获取他的limit地址,即末地址 
  9.       void** limit = &thread_local.Blocks()->last()[i::kHandleBlockSize]; 
  10.       if (current_.limit != limit) { 
  11.         current_.limit = limit; 
  12.         // v8里少了这一句,看起来是需要修改result的值的 
  13.         // result = limit - i::kHandleBlockSize;  
  14.       } 
  15.     } 
  16.   } 
  17.   // 下一个可用的地址 
  18.   current_.next = result + 1; 
  19.   *result = value; 
  20.   return result; 

我们看到CreateHandle会首先获取一片内存,然后把入参value的值保存到该内存中。

String::New

了解了HandleScope后,我们继续分析String::New。

  1. Local<String> v8::String::New(const char* data, int length) { 
  2.   i::Handle<i::String> result = 
  3.       i::Factory::NewStringFromUtf8(i::Vector<const char>(data, length)); 
  4.   return Utils::ToLocal(result); 

我们接着看NewStringFromUtf8。

  1. Handle<String> Factory::NewStringFromUtf8(Vector<const char> string,PretenureFlag pretenure) { 
  2.   CALL_HEAP_FUNCTION(Heap::AllocateStringFromUtf8(string,  
  3.                     pretenure),  
  4.                     String); 

我们先看一下AllocateStringFromUtf8的实现,然后再看CALL_HEAP_FUNCTION。

  1. Object* Heap::AllocateStringFromUtf8(Vector<const char> string,PretenureFlag pretenure) { 
  2.   return AllocateStringFromAscii(string, pretenure); 
  3.  
  4. Object* Heap::AllocateStringFromAscii(Vector<const char> string,PretenureFlag pretenure) { 
  5.   // 从堆中分配一块内存 
  6.   Object* result = AllocateRawAsciiString(string.length(), pretenure); 
  7.   // 设置堆对象的内容 
  8.   AsciiString* string_result = AsciiString::cast(result); 
  9.   for (int i = 0; i < string.length(); i++) { 
  10.     string_result->AsciiStringSet(i, string[i]); 
  11.   } 
  12.   return result; 

我们看到AllocateStringFromUtf8最后返回了一个堆内存地址。接着我们看下CALL_HEAP_FUNCTION这个宏。

  1. #define CALL_HEAP_FUNCTION(FUNCTION_CALL, TYPE)         
  2.   do {                  
  3.     Object* __object__ = FUNCTION_CALL;                                       
  4.     return Handle<TYPE>(TYPE::cast(__object__));    
  5.   } while (false

CALL_HEAP_FUNCTION的作用是把函数FUNCTION_CALL执行的结果转成Handle对象。我们知道FUNCTION_CALL函数返回的结果是一个堆内存指针。接下来我们看看是如何转成Handle的。这个Handle不是我们在代码里使用的Handle。而是V8内部使用的Handle(代码在handles.h),我们看看实现。

  1. template<class T> 
  2. class Handle { 
  3.  public
  4.   explicit Handle(T* obj); 
  5.  private: 
  6.   T** location_; 
  7. }; 
  8.  
  9. template<class T> 
  10. Handle<T>::Handle(T* obj) { 
  11.   location_ = reinterpret_cast<T**>(HandleScope::CreateHandle(obj)); 

我们看到Handle内部使用的是T**二级指针,而我们刚才拿到堆内存地址是一级指针,自然不能直接赋值,而是通过CreateHandle又处理了一下。HandleScope::CreateHandle我们刚才已经分析过了。执行CreateHandle后布局如下。

所以NewStringFromUtf8最后返回了一个Handle对象(里面维护了一个二级指针location_),接着V8调用Utils::ToLocal把他转成外部使用的Handle。接着赋值给Handle hello。这里的Handle是外部使用的Handle。

  1. Local<v8::String> Utils::ToLocal(v8::internal::Handle<v8::internal::String> obj) {  
  2.     return Local<String>(reinterpret_cast<String*>(obj.location()));  

首先通过obj.location()拿到一个二级指针。然后转成一个String *指针。接着构造一个Local对象。ToLocal是V8代码的分水岭,我们看看Local的定义。

  1. template <class T> class Local : public Handle<T> { 
  2.  public
  3.   template <class S> inline Local(S* that) : Handle<T>(that) { } 
  4. }; 

直接调用Handle类的函数

  1. template <class T> class Handle { 
  2.   explicit Handle(T* val) : val_(val) { } 
  3.   private: 
  4.     T* val_; 

这时候的结构图如下

  1. template <class T> class Handle { 
  2.   explicit Handle(T* val) : val_(val) { } 
  3.   private: 
  4.     T* val_; 

所以最后通过ToLocal返回一个外部Handle对象给用户。当执行

  1. Local <String> xxx = Local对象 

时就会调用Local的拷贝函数。

  1. template <class S>  
  2.       inline Local(Local<S> that) 
  3.       // *that即取得他底层对象的地址 
  4.       : Handle<T>(reinterpret_cast<T*>(*that)) {} 

我们首先看一下that。Handle类重载了运算符。

  1. template <class T> 
  2. T* Handle<T>::operator*() { 
  3.   return val_; 

所以reinterpret_cast(that)拿到了Handle底层指针的值并转成String 类型。接着执行

  1. explicit Handle(T* val) : val_(val) { } 

整个过程下来,其实就是把被复制对象的底层指针复制过来。=

通过Handle访问一个函数

当我们使用Handle hello这个对象的方法时是怎样的,比如hello->Length()。Handle重载了->运算符。

  1. template <class T> 
  2. T* Handle<T>::operator->() { 
  3.   return val_; 

我们看到执行hello->Length()的时候首先会拿到一个String *。然后调用Length方法。其实就是调用String对象(在v8.h中定义)的Length方法。我们看看Length方法的实现。

  1. int String::Length() { 
  2.   return Utils::OpenHandle(this)->length(); 

首先通过传入this调用OpenHandle拿到内部Handle。从前面的架构图中我们知道this(即val_和location_指向的值)本质上是一个String **,即二级指针。

  1. v8: :internal: :Handle < v8: :internal: :String >  
  2. Utils: :OpenHandle(v8: :String * that) { 
  3.     return v8: :internal: :Handle < v8: :internal: :String > (reinterpret_cast < v8: :internal: :String * *>(that)); 

OpenHandle就是首先把外部的表示转成一个二级指针。然后再构造一个内部Handle。在内部Handle里保存了这个二级指针。接着访问这个Handle对象的length方法。而Handle重载了->运算符。

  1. INLINE(T* operator ->() const)  { return operator*(); } 
  2.  
  3. template <class T>inline T* Handle<T>::operator*() const { 
  4.   return *location_; 

我们看到->的操作最终会被解引用一次变成String *,然后访问函数length,也就是访问String对象的length函数。

后记:从上面的分析中我们不仅看到了Handle的实现原理,也看到了V8代码的一些设计细节,V8在内部实现了一类对象,然后把内部对象转成外部使用的类型后返回给用户,当用户使用该返回的对象时,V8又会转成内部的对象再操作这个对象。核心的数据结构是两个Handle族的类。因为他们是维护了真实对象的句柄。其他的一些类,比如String,同样分为外部和内部类,内部类是实现了String的细节,而外部类只是一个壳子,他负责给用户暴露API,而不负责实现细节,但用户操作这些类时,V8会会转成内部类再进行操作。外部类的定义在v8.h中,这是我们使用V8时需要了解的最好文档。内部类的实现根据版本不同而不同,比如早期版本都是在object.h里实现的,而实现内外部对象转换的方法在api.c中定义。

 

责任编辑:武晓燕 来源: 编程杂技
相关推荐

2023-06-05 16:38:51

JavaScript编程语言V8

2022-09-16 08:32:25

JavaC++语言

2014-11-26 09:51:24

GithubGoogleV8

2022-10-24 09:11:05

TypeScriptV8

2021-10-22 21:39:11

InspectorV8 JS

2023-10-10 10:23:50

JavaScriptV8

2010-07-20 16:35:52

V8JavaScript浏览器

2020-09-27 07:32:18

V8

2022-08-19 06:40:02

V8GC

2010-08-31 11:42:03

DB2MDC

2022-04-29 08:00:51

V8垃圾回收

2023-02-28 07:56:07

V8内存管理

2011-10-19 13:47:57

ibmdwRationalWAS

2016-10-18 15:18:48

JEECMS V*javaCMS系统

2021-08-29 18:34:44

编译V8C++

2022-05-06 23:03:48

V8CPUProfiler

2022-04-29 08:05:06

内存堆外GC

2021-09-05 17:46:21

云计算No.jsio_uringJS

2020-08-31 08:11:01

V8 8.5Promise前端

2022-06-02 12:02:12

V8C++JavaScript
点赞
收藏

51CTO技术栈公众号