如何正確地使用v8嵌入到咱們的C++應用中

v8學習高級進階以後的實戰.md 繼翻譯了[譯文]V8學習的高級進階以後,相信確定有不少人看得雲裏霧裏的,這個時候就須要這篇針對高級進階的實戰之做,來幫助你們融會貫通。html

接下去高級進階中提到的概念均可以在下面的三個小部分中體現出來。在講述概念以前,咱們依然會有一個v8-demo來幫助咱們理解一些東西。node

一、環境準備

下載v8-demo到本地以後,咱們須要先編譯一份可用的v8庫。c++

1.一、編譯v8

若是有讀過深刻學習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.agithub

v8.png

而後咱們使用CLion新建一個C++工程,目錄以下:shell

v8_1.png

咱們新建的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鍵,就能夠喚出命令行窗口,以下圖網絡

v8_2.png

reload cmake命令能夠生成Makefile文件以及一些附屬文件。這樣咱們就能夠Build這個工程。編譯生成的文件放在cmake-build-debug下:app

v8_3.png

執行對應可執行文件,結果以下:

v8_4.png

熟悉了上面的整套流程以後,咱們開始來講說如何利用v8引擎和Js腳本作些事情。

二、v8引擎基本概念簡述

[譯文]V8學習的高級進階完整詳細地介紹了不少概念,這裏只是再把這些概念簡化掉,讓你們的記憶更加深入。

2.一、isolate

這個概念在[譯文]V8學習的高級進階沒有說起到,它表示的一個獨立的V8虛擬機,擁有本身的堆棧。因此才取名isolate,意爲「隔離」。在v8中使用如下語法進行初始化:

Isolate* isolate = Isolate::New(create_params);
複製代碼

2.二、handle

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);

2.三、context

context 是一個執行器環境,使用 context 能夠將相互分離的 JavaScript 腳本在同一個 V8 實例中運行,而互不干涉。在運行 JavaScript 腳本是,須要顯式的指定 context 對象。建立上下文,須要這樣:

// 建立一個上下文
Local<Context> context = Context::New(isolate);

// 進入上下文編譯和運行腳本
Context::Scope context_scope(context);
複製代碼

2.四、V8的數據類型

因爲 C++ 原生數據類型與 JavaScript 中數據類型有很大差別,所以 V8 提供了 Data 類,從 JavaScript 到 C++,從 C++ 到 JavaScrpt 都會用到這個類及其子類,好比:

String::NewFromUtf8(info.GetIsolate(), "version").ToLocalChecked()
複製代碼

這裏的String即是V8的數據類型。再好比:

v8::Integer::New(info.GetIsolate(), 10);
複製代碼

2.五、對象模板和函數模板

這兩個模板類用以定義 JavaScript 對象和 JavaScript 函數。咱們在後續的小節部分將會接觸到模板類的實例。經過使用 ObjectTemplate,能夠將 C++ 中的對象暴露給腳本環境,相似的,FunctionTemplate 用以將 C++ 函數暴露給腳本環境,以供腳本使用。

三、如何使用v8引擎?

以官方提供的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-demoshell目錄下:

.
├── 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文件,咱們能夠這樣加載該文件並打印結果:

v8_5.png

shell.cc有了個大體瞭解以後咱們開始從demo中抽出模板來講下面的三部分。

四、使用 C++ 變量

C++和Js之間共享模板比較容易,在[譯文]V8學習的高級進階中的第三節訪問器中便提到了C++變量的訪問。其中區分了靜態全局變量和動態變量,咱們在v8-demo中實現了兩者。

4.一、使用全局靜態變量

這個的使用在[譯文]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);
複製代碼

直接運行咱們的實例,操做以下:

v8_6.png

4.二、使用動態變量

使用動態變量的也就是咱們第三小節要講的東西。

二、調用 C++ 函數

在 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;
}
複製代碼

三、使用 C++ 類(動態變量)

這個使用在[譯文]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();
複製代碼

完整流程即是:

  1. 新建對象模板
  2. 對象模板設置內部字段個數
  3. 掛載對象模板的成員變量的訪問器
  4. 新建一個實例對象
  5. 對象模板實例化
  6. 設置實例化後的模板的內部字段索引爲0的指向實例對象
  7. 暴露變量p到js空間下供訪問

那麼若是咱們想要在Js空間下動態建立Point對象呢?那麼即是咱們下面的另一套流程:

3.一、動態建立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.png

在這個實例咱們用到了[譯文]V8學習的高級進階第7節說起到的繼承功能: PrototypeTemplate。另外還有InstanceTemplate。兩者的用途是這樣的:

  1. PrototypeTemplate用於在原型上定義函數或者訪問器
  2. 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()的話,那麼訪問器就不會再繼續執行,而是在攔截器中直接放回了,你能夠去掉上述代碼的註釋試試看哦~

參考

  1. Getting started with embedding V8
  2. Building V8 with GN
  3. 使用 Google V8 引擎開發可定製的應用程序
相關文章
相關標籤/搜索