认识 V8 引擎(4)- 基本概念

上篇 V8 代码编译过之后,接下来就是把 Hello World 跑起来。我们之前已经编译出单个的 V8 静态库:libv8_monolith.a,然后再配合 V8 的头文件,创建一个 main.cc 文件,编译我们的第一个 Hello World。

目录结构如下:

v8-demo
|-- include
|   |-- v8.h
|   |-- ...
|-- libs
|   |-- libv8_monolith.a
|-- src
|   |-- main.cc
|-- CMakeLists.txt

CMakeLists.txt 内容:

cmake_minimum_required(VERSION 3.11)
project (v8_demo)

set(CMAKE_CXX_STANDARD 14)
add_definitions(-DV8_COMPRESS_POINTERS)

link_directories(${CMAKE_SOURCE_DIR}/libs)
include_directories(include)

add_executable(v8_demo src/main.cc)
target_link_libraries(v8_demo v8_monolith)

main.cc 内容:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "include/libplatform/libplatform.h"
#include "include/v8.h"

int main(int argc, char* argv[]) {
  // Initialize V8.
  std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();
  // Create a new Isolate and make it the current one.
  v8::Isolate::CreateParams create_params;
  create_params.array_buffer_allocator =
      v8::ArrayBuffer::Allocator::NewDefaultAllocator();
  v8::Isolate* isolate = v8::Isolate::New(create_params);
  {
    v8::Isolate::Scope isolate_scope(isolate);
    // Create a stack-allocated handle scope.
    v8::HandleScope handle_scope(isolate);
    // Create a new context.
    v8::Local<v8::Context> context = v8::Context::New(isolate);
    // Enter the context for compiling and running the hello world script.
    v8::Context::Scope context_scope(context);
    // Create a string containing the JavaScript source code.
    v8::Local<v8::String> source =
        v8::String::NewFromUtf8(isolate, "'Hello' + ', World!'",
                                v8::NewStringType::kNormal)
            .ToLocalChecked();
    // Compile the source code.
    v8::Local<v8::Script> script =
        v8::Script::Compile(context, source).ToLocalChecked();
    // Run the script to get the result.
    v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
    // Convert the result to an UTF8 string and print it.
    v8::String::Utf8Value utf8(isolate, result);
    printf("%s\n", *utf8);
  }
  // Dispose the isolate and tear down V8.
  isolate->Dispose();
  v8::V8::Dispose();
  v8::V8::ShutdownPlatform();
  delete create_params.array_buffer_allocator;
  return 0;
}

编译,执行:

$ mkdir -p build
$ cd build
$ cmake ..
$ make
$ ./v8-demo
Hello, World!

可以看到,使用 V8 主要分为几步:

  1. 初始化 V8
  2. 创建 v8::Isolate
  3. 创建 v8::Context
  4. 执行 JS
  5. 环境清理

1. 初始化 V8

V8 的初始化在一个进程内,仅且仅可初始化一次。由于我们之前编译的时候选择了:

v8_enable_i18n_support = false
v8_use_external_startup_data = false

所以,初始化的时候,不需要指定 ICU 多语言文件以及外部传入的 Snapshot 的 StartupData 文件。所以,初始化的必要步骤仅下面两步:

std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
v8::V8::InitializePlatform(platform.get());
v8::V8::Initialize();

第一步是初始化传入 v8::Platform。啥是 Platform 呢?

概念1:v8::Platform

v8::Platform 可以定制诸如内存页分配(PageAllocator),任务调度(NumberOfWorkerThreads、CallOnWorkerThread、IdleTasks、PostJob)等规则。详细可查看 v8-platform.h 的接口内容。通常情况下,这些都不需要定制,只使用默认的设置的话,可以通过 v8::platform::NewDefaultPlatform() 创建一个默认的配置。

/**
 * Returns a new instance of the default v8::Platform implementation.
 *
 * The caller will take ownership of the returned pointer. |thread_pool_size|
 * is the number of worker threads to allocate for background jobs. If a value
 * of zero is passed, a suitable default based on the current number of
 * processors online will be chosen.
 * If |idle_task_support| is enabled then the platform will accept idle
 * tasks (IdleTasksEnabled will return true) and will rely on the embedder
 * calling v8::platform::RunIdleTasks to process the idle tasks.
 * If |tracing_controller| is nullptr, the default platform will create a
 * v8::platform::TracingController instance and use it.
 */
V8_PLATFORM_EXPORT std::unique_ptr<v8::Platform> NewDefaultPlatform(
    int thread_pool_size = 0,
    IdleTaskSupport idle_task_support = IdleTaskSupport::kDisabled,
    InProcessStackDumping in_process_stack_dumping =
        InProcessStackDumping::kDisabled,
    std::unique_ptr<v8::TracingController> tracing_controller = {});

从上面的函数定义及注释可以看出,即使用默认的 Platform,也可以定制线程池大小(thread_pool_size),空闲任务(idle_task_suuport),TracingController 等。

2. 创建 v8::Isolate

v8::Isolate::CreateParams create_params;
create_params.array_buffer_allocator =
    v8::ArrayBuffer::Allocator::NewDefaultAllocator();
v8::Isolate* isolate = v8::Isolate::New(create_params);

创建 v8::Isolate 时,需传入 CreateParams,可以指定 JavaScript 里的 ArrayBuffer 的内存分配器,这样可以对 JavaScript 的 ArrayBuffer 内存进行灵活的管理。当然也可以使用默认的分配器,如上面代码的 NewDefaultAllocator

概念2:v8::Isolate

Isolate 字面意思是隔离的意思,在 V8 中表示的是一个隔离的运行时环境,拥有自己的堆内存。多个不同的 Isolate 运行时可以并行执行,互不干扰,数据也完全隔离不可互相访问,就像多个沙箱的运行环境。

通常,在一个线程中只会创建一个 Isolate 实例。创建 Isolate 后,需要进入 Isolate 才可对 Isolate 做进一步操作,在使用完毕后,需要再退出 Isolate。为了方便进入和自动退出,可以使用 v8::Isolate::Scope 进入和自动退出 Isolate。

3. 创建 v8::Context

v8::HandleScope handle_scope(isolate);
// Create a new context.
v8::Local<v8::Context> context = v8::Context::New(isolate);
// Enter the context for compiling and running the hello world script.
v8::Context::Scope context_scope(context);

概念3:v8::Context

可以从具体某个 Isolate 实例中创建一个新的 Context。 v8::Isolate 主要负责堆内存的隔离和管理,有点类似进程的概念。我们知道,需要执行代码的话,还需要有线程的概念。当然 v8 的 Context 和线程也不完全一样。v8::Context 包含了 JavaScript 代码执行所需的上下文信息,包括全局变量、函数等。可以这样简单理解,如果需要执行 JavaScript 代码,则需要有一个 Context 环境。

  • 要点1:一个 Isolate 中可以创建多个 Context。
  • 要点2:同个 Isolate 内的多个 Context 如果要在不同线程执行,需加 v8::Locker
  • 要点3:尽量让 Context 在同个线程中执行,如果需要在不同线程执行,也需要加 v8::Locker

和 Isolate 一样,Context 也需要进入才能正常操作,并在使用完毕后退出,可以使用 v8::Context::Scope 进入和自动退出。

我们注意到,v8::Context::New(isolate) 返回的类型是 v8::Local<v8::Context>。为什么不是返回 v8::Context*? 这和 V8 的内存对象管理有关。一方面,V8 不希望使用者直接访问 V8 对象的裸的指针地址。因为如果这些对象回收后,可能导致内存的非法访问。另一方面,也不利于 V8 在进行垃圾回收时,对 V8 对象在内存中进行移动(compact)。

因此,V8 返回的对象都通过 v8::Handle 来包一层,有点类似智能指针。v8::Local 是当前栈上的临时 handle,退出栈之后将不可再使用。为了让所有临时 handle 能自动回收,需要使用 v8::HandleScope 再包裹一层。

如果某个函数需要把某个 v8::Local 对象作为返回值返回出去,可以使用 v8::EscapableHandleScope 避免返回对象被自动回收:

Local<Array> NewPointArray(int x, int y, int z) {
  v8::Isolate* isolate = v8::Isolate::GetCurrent();

  // We will be creating temporary handles so we use a handle scope.
  v8::EscapableHandleScope handle_scope(isolate);

  // Create a new empty array.
  v8::Local<v8::Array> array = v8::Array::New(isolate, 3);

  // Return an empty result if there was an error creating the array.
  if (array.IsEmpty())
    return v8::Local<v8::Array>();

  // Fill out the values
  array->Set(0, Integer::New(isolate, x));
  array->Set(1, Integer::New(isolate, y));
  array->Set(2, Integer::New(isolate, z));

  // Return the value through Escape.
  return handle_scope.Escape(array);
}

概念4:v8 handle

前面已经讲了 v8::Local 这种临时 handle,除此之外,还包含其他类型的 handle:

  1. Local handle: v8::Local
  2. Persistent handle: v8::UniquePersistent 和 v8::Persistent
  3. v8::Eternal

当某个对象需要持有住,在后来的某个实际再去使用,可以使用 Persistent handle。v8::UniquePersistent 和 v8::Persistent 都是不可复制的。唯一的区别是,v8::UniquePersistent 会在析构时自动解除对该 JS 对象的引用。v8::Persistent 则需要开发者手动调用 Persistent::Reset 解除。

v8::Eternal 使用的很少,用于创建永远不会被 GC 的对象,直到环境被销毁。

4. 执行 JS

v8::Local<v8::String> source =
    v8::String::NewFromUtf8(isolate, "'Hello' + ', World!'",
                            v8::NewStringType::kNormal)
        .ToLocalChecked();
// Compile the source code.
v8::Local<v8::Script> script =
    v8::Script::Compile(context, source).ToLocalChecked();
// Run the script to get the result.
v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
// Convert the result to an UTF8 string and print it.
v8::String::Utf8Value utf8(isolate, result);
printf("%s\n", *utf8);

将 C/C++ 字符串转成 JS 的字符串,需要使用 v8::String::NewFromUtf8。然后通过 v8::Script::Compile 对 JS 源码字符串进行编译,得到 v8::Script 对象,然后执行 Run 方法,执行该代码并拿到 v8::Value 结果。最后,将 v8::Value 转回 v8::String::Utf8Value 字符串对象,并打印。

这里涉及到了 C/C++ 中的基础类型和 V8 JS 对象的互相转换。举例如下:

// bool
v8::Local<v8::Boolean> js_bool_value = v8::Boolean::New(isolate, true);
bool bool_value = js_bool_value->BooleanValue(isolate);

// int
v8::Local<v8::Number> js_number = v8::Integer::New(isolate, 12);
int number = js_number->Int32Value(isolate);

// float
v8::Local<v8::Number> js_float_number = v8::Number::New(isolate, 12.25);
float float_number = static_cast<float>(js_float_number->NumberValue(isolate));

// string
v8::Local<v8::String> js_string_value = v8::String::NewFromUtf8(isolate, "str", v8::NewStringType::kNormal);
v8::String::Utf8Value const utf8(isolate, js_string_value);
std::string string_value(*utf8, utf8.length());

涉及到更复杂的数据结构转换的话,比如 list, map, ArrayBuffer,以及对象映射,到后面再介绍。

5. 环境清理

// Dispose the isolate and tear down V8.
isolate->Dispose();
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete create_params.array_buffer_allocator;

需对 isolate 执行 Dispose 清理,以及整个 V8 环境进行清理。(Context 不需要主动清理,因为 Context 是包含在 isolate 内的)

总结

代码示例见:https://github.com/coderzh/v8-demo

本篇介绍了 V8 的基本使用方法以及几个基本概念,可以此为基础快速了解 V8 引擎的基本使用。后面将逐步深入到 V8 内部,尽请期待。

微信扫一扫交流

作者:CoderZh
微信关注:hacker-thinking (程序员思考者)
本文出处:https://blog.coderzh.com/2021/08/22/v8-4/
文章版权归本人所有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。