v8学习高级进阶以后的实战.md 继翻译了[译文]V8学习的高级进阶以后,相信确定有不少人看得云里雾里的,这个时候就须要这篇针对高级进阶的实战之做,来帮助你们融会贯通。html
接下去高级进阶中提到的概念均可以在下面的三个小部分中体现出来。在讲述概念以前,咱们依然会有一个v8-demo来帮助咱们理解一些东西。node
下载v8-demo到本地以后,咱们须要先编译一份可用的v8库。c++
若是有读过深刻学习nodejs以前须要掌握的知识点这篇文章的童鞋应该对v8编译不陌生,咱们使用第二种编译方式,可是略微不一样的是咱们直接将全部的动态库连接起来,直接生成一个目标文件v8_monolith
,操做命令以下:git
$ alias v8gen=/path/to/v8/tools/dev/v8gen.py // 这一步第一次编译v8的时候已经设置过了,没有设置的能够再整一次
$ v8gen x64.release.sample
$ ninja -C out.gn/x64.release.sample v8_monolith
复制代码
因而你能够在out.gn/x64.release.sample
目录下看到有这么一个文件libv8_monolith.a
:github
而后咱们使用CLion
新建一个C++工程,目录以下:shell
咱们新建的CMakeLists.txt内容以下:数据库
cmake_minimum_required(VERSION 3.2)
project(V8Demo)
include_directories(/Users/linxiaowu/Github/v8/include)
include_directories(/Users/linxiaowu/Github/v8)
link_directories(
/Users/linxiaowu/Github/v8/out.gn/x64.release.sample/obj
)
link_libraries(
v8_monolith
)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++0x -pthread")
set(SOURCE_FILES
./helloworld.cc)
set(CALC_SOURCE
./shell/shell.cpp
./shell/interceptor.cpp
./shell/exposeToJsFuncs.cpp
./shell/exposeToJsVar.cpp
./shell/shell_util.cpp)
add_executable(HelloWorld ${SOURCE_FILES})
add_executable(Shell ${CALC_SOURCE})
复制代码
CMake的语法不是咱们关注的重点,想学习的能够参考:CMake tutorialbash
接着咱们在CLion下按两次Shift
键,就能够唤出命令行窗口,以下图:网络
reload cmake
命令能够生成Makefile文件以及一些附属文件。这样咱们就能够Build这个工程。编译生成的文件放在cmake-build-debug
下:app
执行对应可执行文件,结果以下:
熟悉了上面的整套流程以后,咱们开始来讲说如何利用v8引擎和Js脚本作些事情。
在[译文]V8学习的高级进阶完整详细地介绍了不少概念,这里只是再把这些概念简化掉,让你们的记忆更加深入。
这个概念在[译文]V8学习的高级进阶没有说起到,它表示的一个独立的V8虚拟机,拥有本身的堆栈。因此才取名isolate,意为“隔离”。在v8中使用如下语法进行初始化:
Isolate* isolate = Isolate::New(create_params);
复制代码
handle是指向对象的指针,在V8中,全部的对象都经过handle来引用,handle主要用于V8的垃圾回收机制。在 V8 中,handle 分为两种:持久化 (Persistent)handle 和本地 (Local)handle,持久化 handle 存放在堆上,而本地 handle 存放在栈上。好比我要使用本地句柄,句柄指向的内容是一个string,那么你要这么定义:
Local<String> source = String::NewFromUtf8(isolate, "'Hello' + ', World'", NewStringType::kNormal).ToLocalChecked();
鉴于一个个释放Handle比较麻烦,v8又提供了HandleScope
来批量处理,你能够在handle以前声明好:
HandleScope handle_scope(isolate);
context 是一个执行器环境,使用 context 能够将相互分离的 JavaScript 脚本在同一个 V8 实例中运行,而互不干涉。在运行 JavaScript 脚本是,须要显式的指定 context 对象。建立上下文,须要这样:
// 建立一个上下文
Local<Context> context = Context::New(isolate);
// 进入上下文编译和运行脚本
Context::Scope context_scope(context);
复制代码
因为 C++ 原生数据类型与 JavaScript 中数据类型有很大差别,所以 V8 提供了 Data 类,从 JavaScript 到 C++,从 C++ 到 JavaScrpt 都会用到这个类及其子类,好比:
String::NewFromUtf8(info.GetIsolate(), "version").ToLocalChecked()
复制代码
这里的String即是V8的数据类型。再好比:
v8::Integer::New(info.GetIsolate(), 10);
复制代码
这两个模板类用以定义 JavaScript 对象和 JavaScript 函数。咱们在后续的小节部分将会接触到模板类的实例。经过使用 ObjectTemplate,能够将 C++ 中的对象暴露给脚本环境,相似的,FunctionTemplate 用以将 C++ 函数暴露给脚本环境,以供脚本使用。
以官方提供的Hello world为例子,咱们将其注释helloworld.cc,并写上对应的步骤:
一、初始化V8
二、建立一个新的隔离区,并将这个隔离区置为当前使用
三、建立一个栈分配的句柄范围
四、建立一个上下文
五、进入上下文编译和运行脚本
六、销毁isolate以及使用过的buffer,并关掉进程
复制代码
其对应的代码以下:
int main(int argc, char * argv[]) {
// 一、初始化V8
V8::InitializeICUDefaultLocation(argv[0]);
V8::InitializeExternalStartupData(argv[0]);
unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
V8::InitializePlatform(platform.get());
V8::Initialize();
// 二、建立一个新的隔离区,并将这个隔离区置为当前使用
Isolate::CreateParams create_params;
create_params.array_buffer_allocator = ArrayBuffer::Allocator::NewDefaultAllocator();
Isolate* isolate = Isolate::New(create_params);
{
Isolate::Scope isolate_scope(isolate);
// 三、建立一个栈分配的句柄范围
HandleScope handle_scope(isolate);
// 四、建立一个上下文
Local<Context> context = Context::New(isolate);
// 五、进入上下文编译和运行脚本
Context::Scope context_scope(context);
{
Local<String> source = String::NewFromUtf8(isolate, "'Hello' + ', World'", NewStringType::kNormal).ToLocalChecked();
Local<Script> script = Script::Compile(context, source).ToLocalChecked();
Local<Value> result = script->Run(context).ToLocalChecked();
String::Utf8Value utf8(isolate, result);
printf("%s\n", *utf8);
}
}
// 六、销毁isolate以及使用过的buffer,并关掉进程
isolate->Dispose();
V8::Dispose();
V8::ShutdownPlatform();
delete create_params.array_buffer_allocator;
return 0;
}
复制代码
有了上面的基础,看懂这些代码应该不在话下了。那么接下去,咱们要探讨的是如何利用v8和js脚本作些事情呢?咱们以官网的shell.cc为例,将其改造后拆分文件并放到v8-demo
的shell
目录下:
.
├── exposeToJsFuncs.cpp => 存放那些暴露给Js脚本使用的函数原型
├── exposeToJsVar.cpp => 存放那些暴露给Js脚本使用的变量访问器
├── interceptor.cpp => 拦截器存放的地方
├── load.js => 演示在js中加载的脚本文件
├── shell.cpp => 主文件
├── shell.h => 头文件
└── shell_util.cpp => 其余有用的函数存放的地方
复制代码
shell.cc
提供了一个简易版本的CLI,在该CLI中能够执行js脚本并输出结果,也能够加载js文件进去供CLI执行,好比demo中提供了load.js
文件,咱们能够这样加载该文件并打印结果:
对shell.cc
有了个大体了解以后咱们开始从demo中抽出模板来讲下面的三部分。
C++和Js之间共享模板比较容易,在[译文]V8学习的高级进阶中的第三节访问器中便提到了C++变量的访问。其中区分了静态全局变量和动态变量,咱们在v8-demo中实现了两者。
这个的使用在[译文]V8学习的高级进阶讲得比较清楚,其流程应该是:
一、定义全局静态变量,demo中咱们定义了version
这个变量:char version[100];
二、定义全局静态变量的Getter/Setter方法,方法定义在文件exposeToJsVar:
void VersionGetter(Local<String> property,
const PropertyCallbackInfo<Value> &info) {
info.GetReturnValue().Set(String::NewFromUtf8(info.GetIsolate(), version).ToLocalChecked());
}
void VersionSetter(Local<String> property, Local<Value> value, const PropertyCallbackInfo<void> &info) {
String::Utf8Value str(info.GetIsolate(), value);
const char *result = ToCString(str);
strncpy(version, result, sizeof(version));
}
复制代码
记得其通用的函数签名:(Local<String> property, const PropertyCallbackInfo<Value> &info)
和(Local<String> property, Local<Value> value, const PropertyCallbackInfo<void> &info)
三、声明一个全局对象:Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
四、挂载version的Getter方法和Setter方法到全局对象上,并以version
这个名字暴露给Js使用:
global->SetAccessor(String::NewFromUtf8(isolate, "version").ToLocalChecked(), VersionGetter, VersionSetter);
复制代码
直接运行咱们的实例,操做以下:
使用动态变量的也就是咱们第三小节要讲的东西。
在 JavaScript 中调用 C++ 函数是脚本化最多见的方式,经过使用 C++ 函数,能够极大程度的加强 JavaScript 脚本的能力,如文件读写,网络 / 数据库访问,图形 / 图像处理等等,而在 V8 中,调用 C++ 函数也很是的方便。
首先在C++中定义原型函数Print
:
void Print(const FunctionCallbackInfo <Value> &args) {
bool first = true;
for (int i = 0; i < args.Length(); i++) {
HandleScope handle_scope(args.GetIsolate());
if (first) {
first = false;
} else {
printf(" ");
}
String::Utf8Value str(args.GetIsolate(), args[i]);
const char *cstr = ToCString(str);
printf("%s", cstr);
}
printf("\n");
fflush(stdout);
}
复制代码
这一类函数都有一样的函数签名:(const FunctionCallbackInfo &args)。
而后将其暴露到Js环境下:
Local<ObjectTemplate> global = ObjectTemplate::New(isolate);
global->Set(
String::NewFromUtf8(isolate, "print", NewStringType::kNormal)
.ToLocalChecked(),
FunctionTemplate::New(isolate, Print));
复制代码
因而咱们就能够在Js环境下调用print
函数了,在以前的图五中已经有展现过print
函数的使用了:
function sum(){
var s = 0;
for(var i = 0; i < arguments.length; i++){
print(arguments[i])
s += arguments[i];
}
return s;
}
复制代码
这个使用在[译文]V8学习的高级进阶讲得也还算清楚,惟一让人混淆的是原文并无提供出如何访问定义的C++类,并且文章的示例也是不够“动态”,由于只支持在C++中定义好,不支持在js脚本中动态建立。因而咱们将其改造了一番。
在说改造以前,咱们先清楚若是使用动态变量,在咱们的v8-demo中,shell.cpp
有定义一个宏,里面的代码即是原文的示例:
Local<ObjectTemplate> point_templ = ObjectTemplate::New(isolate);
point_templ->SetInternalFieldCount(1);
point_templ->SetAccessor(String::NewFromUtf8(isolate, "x").ToLocalChecked(), GetPointX, SetPointX);
point_templ->SetAccessor(String::NewFromUtf8(isolate, "y").ToLocalChecked(), GetPointY, SetPointY);
Point* p = new Point(11, 22);
Local<Object> obj = point_templ->NewInstance(context).ToLocalChecked();
obj->SetInternalField(0, External::New(isolate, p));
context->Global()->Set(context, String::NewFromUtf8(isolate, "p").ToLocalChecked(), obj).ToChecked();
复制代码
完整流程即是:
那么若是咱们想要在Js空间下动态建立Point对象呢?那么即是咱们下面的另一套流程:
首先咱们在shell.h
中定义好Point
的类:
class Point {
public:
Point(int x, int y) : x_(x), y_(y) { }
int x_, y_;
int multi() {
return this->x_ * this->y_;
}
};
复制代码
类有两个成员变量和一个成员函数。接着咱们对Point的构造器进行包装:
void constructPoint(const FunctionCallbackInfo <Value> &args) {
Isolate* isolate = Isolate::GetCurrent();
//get an x and y
double x = args[0]->NumberValue(isolate->GetCurrentContext()).ToChecked();
double y = args[1]->NumberValue(isolate->GetCurrentContext()).ToChecked();
//generate a new point
Point *point = new Point(x, y);
args.This()->SetInternalField(0, External::New(isolate, point));
}
复制代码
从函数原型上能够看出,构造器的包装与上一小节中函数的包装是一致的,由于构造函数在 V8 看来,也是一个函数。须要注意的是,从 args 中获取参数并转换为合适的类型以后,咱们根据此参数来调用 Point 类实际的构造函数,并将其设置在 object 的内部字段中。紧接着,咱们须要包装 Point 类的 getter/setter:
void GetPointX(Local<String> property,
const PropertyCallbackInfo<Value> &info) {
printf("GetPointX is calling\n");
Local<Object> self = info.Holder();
Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
void* ptr = wrap->Value();
int value = static_cast<Point*>(ptr)->x_;
info.GetReturnValue().Set(value);
}
void SetPointX(Local<String> property, Local<Value> value, const PropertyCallbackInfo<void> &info) {
printf("SetPointX is calling\n");
Local<Object> self = info.Holder();
Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
void* ptr = wrap->Value();
static_cast<Point*>(ptr)->x_ = value->Int32Value(info.GetIsolate()->GetCurrentContext()).ToChecked();
}
复制代码
以及对Point
类成员方法的包装:
void PointMulti(const FunctionCallbackInfo <Value> &args) {
Isolate* isolate = Isolate::GetCurrent();
//start a handle scope
HandleScope handle_scope(isolate);
Local<Object> self = args.Holder();
Local<External> wrap = Local<External>::Cast(self->GetInternalField(0));
void* ptr = wrap->Value();
// 这里直接调用已经实例化的Point类的成员方法multi,并拿到结果
int value = static_cast<Point*>(ptr)->multi();
args.GetReturnValue().Set(value);
}
复制代码
在对函数包装完成以后,须要将 Point 类暴露给脚本环境:
Handle<FunctionTemplate> point_templ = FunctionTemplate::New(isolate, constructPoint);
point_templ->SetClassName(String::NewFromUtf8(isolate, "Point").ToLocalChecked());
// 挂载Point类到全局对象中,保证可用
global->Set(String::NewFromUtf8(
isolate, "Point", NewStringType::kNormal).ToLocalChecked(), point_templ);
复制代码
而后定义原型模板:
//初始化原型模板
Handle<ObjectTemplate> point_proto = point_templ->PrototypeTemplate();
// 原型模板上挂载multi方法
point_proto->Set(String::NewFromUtf8(
isolate, "multi", NewStringType::kNormal).ToLocalChecked(), FunctionTemplate::New(isolate, PointMulti));
复制代码
接着实例化模板:
// 初始化实例模板
Handle<ObjectTemplate> point_inst = point_templ->InstanceTemplate();
//set the internal fields of the class as we have the Point class internally
point_inst->SetInternalFieldCount(1);
//associates the name "x"/"y" with its Get/Set functions
point_inst->SetAccessor(String::NewFromUtf8(isolate, "x").ToLocalChecked(), GetPointX, SetPointX);
point_inst->SetAccessor(String::NewFromUtf8(isolate, "y").ToLocalChecked(), GetPointY, SetPointY);
复制代码
因而咱们能够轻松地在Js环境下使用Point
类,而不用去关注Point
类的定义:
在这个实例咱们用到了[译文]V8学习的高级进阶第7节说起到的继承功能: PrototypeTemplate
。另外还有InstanceTemplate
。两者的用途是这样的:
最后一个要说的是拦截器,正如原文所说的,拦截器是针对全部属性的,而访问器是针对个别属性的,因而咱们在上面的示例中添加一个拦截器,注意:原文中拦截器的方法已经废弃!!请使用下文中的示例
// 给访问x设置一个拦截器吧
point_inst->SetHandler(NamedPropertyHandlerConfiguration(PointInterceptorGetter, PointInterceptorSetter));
复制代码
拦截器的定义以下:(在interceptor.cpp文件中)
void PointInterceptorGetter(
Local<Name> name, const PropertyCallbackInfo<Value>& info) {
if (name->IsSymbol()) return;
// Fetch the map wrapped by this object.
map<string, string>* obj = UnwrapMap(info.Holder());
// Convert the JavaScript string to a std::string.
string key = ObjectToString(info.GetIsolate(), Local<String>::Cast(name));
printf("interceptor Getting for Point property has called, name[%s]\n", key.c_str());
// 若是调用这个设置return,那么就不会再执行后面的Getter
// info.GetReturnValue().Set(11);
}
复制代码
在上图中,咱们看到当访问p1.x
的时候,都会打印拦截器的printf函数。这代表拦截器生效了。另外须要注意的是:
若是你再拦截器中调用info.GetReturnValue()的话,那么访问器就不会再继续执行,而是在拦截器中直接放回了,你能够去掉上述代码的注释试试看哦~