当前位置: 首页 > 科技观察

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

时间:2023-03-20 10:57:11 科技观察

本文转载自微信公众号《编程杂技》,作者theanarkh。转载本文请联系编程杂技公众号。前言:Handle是V8中一个非常重要的概念。本文从早期的源码分析Handle的原理。在分析过程中,我们还可以看到V8的代码设计的一些细节。假设我们有以下代码HandleScopescope;Localhello=String::New(parameter);这个看似简单的过程在V8内部实现起来其实比较复杂。HandleScope我们通过创建一个HandleScope对象开始分析。HandleScope是一个负责管理多个Handle的对象,主要是为了方便管理Handle的分配和释放。classHandleScope{public:HandleScope():previous_(current_),is_closed_(false){current_.extensions=0;}staticvoid**CreateHandle(void*value);private:classData{public://分配一块内存后,andTheextraallocatedblocksintexttensions;//下一个可用位置void**next;//到达limit执行地址后,表示当前内存块用完void**limit;inlinevoidInitialize(){extensions=-1;next=limit=NULL;}};//当前HandleScopestaticDatacurrent_;//前一个HandleScopeconstDataprevious_;};HandleScope::DataHandleScope::current_={-1,NULL,NULL};通过HandleScope的构造函数我们知道,每次定义一个HandleScope对象,previous都会指向上一个HandleScope的数据(但是current_除了第一次创建HandleScope的时候都会更新(见CreateHandle),貌似以后还没有更新吗?后面再详细看),从HandleScope的定义我们知道布局如下。然后我们看HandleScope的CreateHandle方法。void**v8::HandleScope::CreateHandle(void*value){//获取下一个可用地址void**result=current_.next;//获取到限制地址或为空(初始化时)新建内存if(result==current_.limit){//Block是一个二维数组,每个元素指向一个可以存放数据的数组。非空表示可能有可用的内存空间,也就是结束地址void**limit=&thread_local.Blocks()->last()[i::kHandleBlockSize];if(current_.limit!=limit){current_.limit=limit;//少了这一句在v8中,貌似需要修改result的值//result=limit-i::kHandleBlockSize;}}}//下一个可用地址current_.next=result+1;*result=value;returnresult;我们看到CreateHandle会先获取一块内存,然后将输入参数值的值保存到内存中。String::New理解了HandleScope之后,我们继续分析String::New。Localv8::String::New(constchar*data,intlength){i::Handleresult=i::Factory::NewStringFromUtf8(i::Vector(data,length));returnUtils::ToLocal(result);}接下来我们看一下NewStringFromUtf8。HandleFactory::NewStringFromUtf8(Vectorstring,PretenureFlagpretenure){CALL_HEAP_FUNCTION(Heap::AllocateStringFromUtf8(string,pretenure),String);}我们先看AllocateStringFromUtf8的实现,再看CALL_HEAP_FUNCTION。Object*Heap::AllocateStringFromUtf8(Vectorstring,PretenureFlagpretenure){returnAllocateStringFromAscii(string,pretenure);}Object*Heap::AllocateStringFromAscii(Vectorstring,PretenureFlagpretenure){//从堆中分配一块内存Object*result=AllocateRawAsciiString(string.length(),pretenure);//设置堆对象的内容AsciiString*string_result=AsciiString::cast(result);for(inti=0;iAsciiStringSet(i,string[i]);}returnresult;}我们看到AllocateStringFromUtf8最后返回了一个堆内存地址。然后我们查看CALL_HEAP_FUNCTION宏。#defineCALL_HEAP_FUNCTION(FUNCTION_CALL,TYPE)do{Object*__object__=FUNCTION_CALL;returnHandle(TYPE::cast(__object__));}while(false)CALL_HEAP_FUNCTION的作用是将函数FUNCTION_CALL执行的结果转换成一个处理对象。我们知道FUNCTION_CALL函数返回的结果是一个堆内存指针。接下来我们看看它是如何转化为Handle的。这个Handle不是我们在代码中使用的Handle。是V8内部使用的Handle(代码在handles.h中),我们看一下实现。templateclassHandle{public:explicitHandle(T*obj);private:T**location_;};templateHandle::Handle(T*obj){location_=reinterpret_cast(HandleScope::CreateHandle(obj));}我们看到Handle内部使用了T**二级指针,而我们刚刚得到的堆内存地址是一级指针。自然不能直接赋值,而是通过CreateHandle进行一次处理。HandleScope::CreateHandle我们刚刚分析过。执行完CreateHandle后布局如下。所以NewStringFromUtf8最后返回一个Handle对象(其中维护了一个二级指针location_),然后V8调用Utils::ToLocal将其转换为外部使用的Handle。然后赋值给Handlehello。这里的Handle是对外使用的Handle。LocalUtils::ToLocal(v8::internal::Handleobj){returnLocal(reinterpret_cast(obj.location()));}先通过obj.location()得到一个二级指针。然后把它变成一个String*指针。然后构造一个Local对象。ToLocal是V8代码的分水岭,我们来看一下Local的定义。templateclassLocal:publicHandle{public:templateinlineLocal(S*that):Handle(that){}};直接调用Handle类的函数templateclassHandle{explicitHandle(T*val):val_(val){}private:T*val_;}此时的结构图如下(T*val):val_(val){}private:T*val_;}所以最后通过ToLocal返回一个外部Handle对象给用户。当Localxxx=Local对象被执行时,会调用Localcopy函数。templateinlineLocal(Localthat)//*that获取其底层对象的地址:Handle(reinterpret_cast(*that)){}我们先来看一下。Handle类具有重载的运算符。templateT*Handle::operator*(){returnval_;}所以reinterpret_cast(that)获取底层Handle指针的值,并将其转换为String类型。然后执行explicitHandle(T*val):val_(val){}整个过程其实就是复制被复制对象的底层指针。=通过Handle访问函数当我们使用Handlehello对象的方法时会发生什么,比如hello->Length()。句柄重载->运算符。templateT*Handle::operator->(){returnval_;}我们看到执行hello->Length()时,首先会得到一个String*。然后调用Length方法。其实就是调用String对象(在v8.h中定义)的Length方法。让我们看一下Length方法的实现。intString::Length(){returnUtils::OpenHandle(this)->length();}首先调用OpenHandle通过传入this获取内部Handle。从前面的架构图我们知道this(也就是val_和location_指向的值)本质上是一个String**,也就是二级指针。v8::internal::HandleUtils::OpenHandle(v8::String*that){returnv8::internal::Handle(reinterpret_cast(that));}OpenHandle是先将外部表示转化为二级指针。然后构造一个内部句柄。这个二级指针保存在内部句柄中。然后访问Handle对象的length方法。Handle重载了->运算符。INLINE(T*operator->()const){returnoperator*();}templateinlineT*Handle::operator*()const{return*location_;}我们看到->的操作会eventually被解引用一次,变成String*,然后访问函数length,即访问String对象的length函数。后记:从上面的分析中,我们不仅看到了Handle的实现原理,还看到了V8代码的一些设计细节。V8内部实现了一类对象,然后将内部对象转换成外部使用的类型返回给用户。当用户使用返回的对象时,V8会把它变成一个内部对象,然后对这个对象进行操作。核心数据结构是两个Handle家族的类。因为它们是维护真实对象的句柄。其他一些类,比如String,也分为外部类和内部类。内部类实现了String的细节,而外部类只是一个壳。它负责将API暴露给用户,而不是实现细节,而是用户操作这些类,V8会将其转化为内部类再进行操作。外部类的定义在v8.h中,这是我们在使用V8时需要知道的最好的文档。内部类的实现因版本而异。比如早期版本是在object.h中实现的,在api.c中定义了实现内外对象转换的方法。