将v8当成工具使用

将v8变成工具

如何嵌入一个v8引擎到你的应用中

v8除了可以作为一个独立的js引擎之外,还可以通过库的方式嵌入到我们的应用中,它以V8 API的方式服务我们。

我们来看一个老一点的例子来看看v8 API是如何使用的:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "include/libplatform/libplatform.h"
#include "include/v8.h"
int main(int argc, char* argv[]) {
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  v8::V8::InitializeExternalStartupData(argv[0]);
  std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();
  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);
    v8::HandleScope handle_scope(isolate);
    v8::Local<v8::Context> context = v8::Context::New(isolate);
    v8::Context::Scope context_scope(context);
    v8::Local<v8::String> source =
        v8::String::NewFromUtf8(isolate, "let a = 2 ** 8; a++;",
                                v8::NewStringType::kNormal)
            .ToLocalChecked();
    v8::Local<v8::Script> script =
        v8::Script::Compile(context, source).ToLocalChecked();
    v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
    v8::String::Utf8Value utf8(isolate, result);
    printf("%s\n", *utf8);
  }
  isolate->Dispose();
  v8::V8::Dispose();
  v8::V8::ShutdownPlatform();
  delete create_params.array_buffer_allocator;
  return 0;
}

官方的例子是输出一个"Hello,World"字符串。我觉得这完全没有展示出这个例子的强大之处,因为能拼接字符串是个非常常规的操作,而我们引进来的是可以解析js代码的引擎。不写几条js语句,真对不起这个例子。

我们将刚才的例子存为hello2.cpp, 编译:

g++ -I. -Iinclude ./hello2.cc -o hello_world2 -lv8_monolith -Lout.gn/x64.release.sample/obj/ -lpthread -std=c++14 -DV8_COMPRESS_POINTERS

v8_monolith这个库怎么编译出来的,我们后面讲编译v8源代码的时候会讲。

然后运行生成的hello_world2,输出:

256

总体流程就是:

  • 初始化ICU
  • 初始化v8平台
  • 创建分配器
  • 创建Isolate
  • 创建Scope
  • 创建Context
  • 创建脚本
  • 编译脚本
  • 运行脚本
  • 清理Isolate
  • 清理v8
  • 清理分配器

其中,Isolate代表一个线程不安全的v8运行实例。其它概念大家基本都可以理解。

新一点的API结构上有一些变化,我们来看下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "include/libplatform/libplatform.h"
#include "include/v8-context.h"
#include "include/v8-initialization.h"
#include "include/v8-isolate.h"
#include "include/v8-local-handle.h"
#include "include/v8-primitive.h"
#include "include/v8-script.h"

int main(int argc, char* argv[]) {
  v8::V8::InitializeICUDefaultLocation(argv[0]);
  v8::V8::InitializeExternalStartupData(argv[0]);
  std::unique_ptr<v8::Platform> platform = v8::platform::NewDefaultPlatform();
  v8::V8::InitializePlatform(platform.get());
  v8::V8::Initialize();
  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);
    v8::HandleScope handle_scope(isolate);
    v8::Local<v8::Context> context = v8::Context::New(isolate);
    v8::Context::Scope context_scope(context);
    {
      v8::Local<v8::String> source =
          v8::String::NewFromUtf8Literal(isolate, "let f1 = (x) => x*x; f1(1.2);");
      v8::Local<v8::Script> script =
          v8::Script::Compile(context, source).ToLocalChecked();
      v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
      v8::String::Utf8Value utf8(isolate, result);
      printf("%s\n", *utf8);
    }
  }
  isolate->Dispose();
  v8::V8::Dispose();
  v8::V8::ShutdownPlatform();
  delete create_params.array_buffer_allocator;
  return 0;
}

换个文件名存,用刚才一样的参数去编译:

g++ -I. -Iinclude ./hello3.cc -o hello_world3 -lv8_monolith -Lout.gn/x64.release.sample/obj/ -lpthread -std=c++14 -DV8_COMPRESS_POINTERS

虽然API变化了,但是都还是被支持的。

总体上,我们发现流程上基本没有太大变化。只是API拆解得更细了。原来只要包含一个v8.h就够了,现在context, isolate, primitive, script等都拆分成独立的API了。

上面的版本是经过我加工过的,其实还有wasm的部分被我删除掉了。在2021年末的这个时刻,wasm的知识对于很多前端同学还不是必备的基础。后面我们有专文讨论wasm基础加上v8的实现。

本地句柄

作为一个JavaScript引擎,v8自然是拥有一个运行时的垃圾回收器。垃圾回收器会回收一切没有句柄持有的对象。

最简单的句柄叫做本地句柄,它是跟栈绑定的。当退出一个作用域时,本地局柄所持有的对象也将都被释放掉。

本地句柄的用法是v8::Local<类型>

上面例子中我们基本上使用的都是本地句柄,代码字符吕、脚本、值都是保存在本地句柄中:

      v8::Local<v8::String> source =
          v8::String::NewFromUtf8Literal(isolate, "let f1 = (x) => x*x; f1(1.2);");
      v8::Local<v8::Script> script =
          v8::Script::Compile(context, source).ToLocalChecked();
      v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
      v8::String::Utf8Value utf8(isolate, result);

针对于可能失败,为空或者出现异常的情况,我们可以使用MaybeLocal,例如:v8::MaybeLocal<v8::String>.

从utf-8字符串构造v8 String的v8::String::NewFromUtf8函数,默认生成的就是v8::MaybeLocal<v8::String>类型。

从MaybeLocal转换到Local,可以调用ToLocalChecked函数。其原型为:

  V8_INLINE Local<T> ToLocalChecked() {
    if (V8_UNLIKELY(val_ == nullptr)) api_internal::ToLocalEmpty();
    return Local<T>(val_);
  }

v8字符串

v8::String所表示的就是JavaScript的字符串。

上节我们说了,要生成v8 String, 需要使用v8::String::NewFromUtf8方法。NewStringType就用kNormal就好:

      char *str1 = "hello,v8";
      v8::MaybeLocal<v8::String> v8str1 =
          v8::String::NewFromUtf8(isolate, str1, v8::NewStringType::kNormal, strlen(str1));

要将v8字符串转换成C字符串,需要通过v8::String::Utf8Value类型来转换。

      v8::String::Utf8Value v8str2(isolate, v8str1.ToLocalChecked());
      printf("%s\n", *v8str2);

我们来看个将文件读到v8字符串的例子:

v8::MaybeLocal<v8::String> ReadFile(v8::Isolate* isolate, const char* name) {
  FILE* file = fopen(name, "rb");
  if (file == NULL) return v8::MaybeLocal<v8::String>();

  fseek(file, 0, SEEK_END);
  size_t size = ftell(file);
  rewind(file);

  char* chars = new char[size + 1];
  chars[size] = '\0';
  for (size_t i = 0; i < size;) {
    i += fread(&chars[i], 1, size - i, file);
    if (ferror(file)) {
      fclose(file);
      return v8::MaybeLocal<v8::String>();
    }
  }
  fclose(file);
  v8::MaybeLocal<v8::String> result = v8::String::NewFromUtf8(
      isolate, chars, v8::NewStringType::kNormal, static_cast<int>(size));
  delete[] chars;
  return result;
}

我们使用上面的ReadFile函数,就可以改成运行文件上的js脚本了:

    {
      v8::Local<v8::String> source;
      if (!ReadFile(isolate, filename).ToLocal(&source)) {
        printf("Cannot read from %s\n", filename);
      }
      v8::Local<v8::Script> script =
          v8::Script::Compile(context, source).ToLocalChecked();
      v8::Local<v8::Value> result = script->Run(context).ToLocalChecked();
      v8::String::Utf8Value utf8(isolate, result);
      printf("%s\n", *utf8);
    }

不过要注意,非Javascript字符串就不要来凑热闹了。
比如要获取v8的版本号,没有这么麻烦,直接就是const char*:

printf("%s\n", v8::V8::GetVersion());

Context

除了内存管理之外,另外一个很重要的事情就是运行上下文Context. 我们可以看到,在代码中,编译需要上下文,运行需要上下文。

我们知道,js默认是提供很多全局变量和全局函数,这些都存储于上下文中。脚本中的对象、函数也都是跟上下文绑定在一起的。

我们来看一下官方的图:


image

编译v8

下面我们就再说下如何编译v8.

下载v8源代码

v8是个比较复杂的工程,只从github上下载 https://github.com/v8/v8 的代码是没法编译的。
我们以mac和Linux为例。

首先我们需要下载depot_tools工具包:

git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git

然后将这个工具包加到path中,后面下载所用的fetch, 编译所用的gn等工具都在这里。

export PATH=/path/to/depot_tools:$PATH

第三步,我们都过fetch工具下载v8:

fetch v8

如果不想用fetch工具,想直接手动下载的话,需要下载以下的库到相应的目录下。
比如我把v8的路径换成github上的了。

entries = {
  'v8': 'https://github.com/v8/v8.git',
  'v8/base/trace_event/common': 'https://chromium.googlesource.com/chromium/src/base/trace_event/common.git@68d816952258c9d817bba656ee2664b35507f01b',
  'v8/build': 'https://chromium.googlesource.com/chromium/src/build.git@f78b0bd09847b94e9ec9cb520855d6785fd082ab',
  'v8/buildtools': 'https://chromium.googlesource.com/chromium/src/buildtools.git@a9bc3e283182a586998338a665c7eae17406ec54',
  'v8/buildtools/clang_format/script': 'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/clang/tools/clang-format.git@99876cacf78329e5f99c244dbe42ccd1654517a0',
  'v8/buildtools/mac:gn/gn/mac-${arch}': 'https://chrome-infra-packages.appspot.com/gn/gn/mac-${arch}@git_revision:693f9fb87e4febdd4299db9f73d8d2c958e63148',
  'v8/buildtools/third_party/libc++/trunk': 'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxx.git@79a2e924d96e2fc1e4b937c42efd08898fa472d7',
  'v8/buildtools/third_party/libc++abi/trunk': 'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libcxxabi.git@9eb0245224c2d7f6b20f76d4d24eab1d60a2b281',
  'v8/buildtools/third_party/libunwind/trunk': 'https://chromium.googlesource.com/external/github.com/llvm/llvm-project/libunwind.git@557b51a0ccab9b3dbce61bdd57aa5f7d5c7c6206',
  'v8/test/benchmarks/data': 'https://chromium.googlesource.com/v8/deps/third_party/benchmarks.git@05d7188267b4560491ff9155c5ee13e207ecd65f',
  'v8/test/mozilla/data': 'https://chromium.googlesource.com/v8/deps/third_party/mozilla-tests.git@f6c578a10ea707b1a8ab0b88943fe5115ce2b9be',
  'v8/test/test262/data': 'https://chromium.googlesource.com/external/github.com/tc39/test262.git@8d420cef415f3501cb24d674b8c032d1f09402a0',
  'v8/test/test262/harness': 'https://chromium.googlesource.com/external/github.com/test262-utils/test262-harness-py.git@278bcfaed0dcaa13936831fb1769d15e7c1e3b2b',
  'v8/third_party/depot_tools': 'https://chromium.googlesource.com/chromium/tools/depot_tools.git@756e98f5aac7fb163e558a5a5cc5f3dc0098b1d7',
  'v8/third_party/google_benchmark/src': 'https://chromium.googlesource.com/external/github.com/google/benchmark.git@1e3ab7fa434d1b4aebdd22b760dbf99c498ae7cd',
  'v8/third_party/googletest/src': 'https://chromium.googlesource.com/external/github.com/google/googletest.git@075810f7a20405ea09a93f68847d6e963212fa62',
  'v8/third_party/icu': 'https://chromium.googlesource.com/chromium/deps/icu.git@4df07a2d158218b77369b82f9fe3190725beb815',
  'v8/third_party/instrumented_libraries': 'https://chromium.googlesource.com/chromium/src/third_party/instrumented_libraries.git@6527a4e98a746f5324e21e813a41af25419bfae7',
  'v8/third_party/jinja2': 'https://chromium.googlesource.com/chromium/src/third_party/jinja2.git@ee69aa00ee8536f61db6a451f3858745cf587de6',
  'v8/third_party/jsoncpp/source': 'https://chromium.googlesource.com/external/github.com/open-source-parsers/jsoncpp.git@9059f5cad030ba11d37818847443a53918c327b1',
  'v8/third_party/logdog/logdog': 'https://chromium.googlesource.com/infra/luci/luci-py/client/libs/logdog@17ec234f823f7bff6ada6584fdbbee9d54b8fc58',
  'v8/third_party/markupsafe': 'https://chromium.googlesource.com/chromium/src/third_party/markupsafe.git@1b882ef6372b58bfd55a3285f37ed801be9137cd',
  'v8/third_party/perfetto': 'https://android.googlesource.com/platform/external/perfetto.git@aa4385bc5997ecad4c633885e1b331b1115012fb',
  'v8/third_party/protobuf': 'https://chromium.googlesource.com/external/github.com/google/protobuf@6a59a2ad1f61d9696092f79b6d74368b4d7970a3',
  'v8/third_party/zlib': 'https://chromium.googlesource.com/chromium/src/third_party/zlib.git@6da1d53b97c89b07e47714d88cab61f1ce003c68',
  'v8/tools/clang': 'https://chromium.googlesource.com/chromium/src/tools/clang.git@c00aa10009548ad073810d810cc4a71d2965f75b',
  'v8/tools/clang/dsymutil:chromium/llvm-build-tools/dsymutil': 'https://chrome-infra-packages.appspot.com/chromium/llvm-build-tools/dsymutil@M56jPzDv1620Rnm__jTMYS62Zi8rxHVq7yw0qeBFEgkC',
  'v8/tools/luci-go:infra/tools/luci/isolate/${platform}': 'https://chrome-infra-packages.appspot.com/infra/tools/luci/isolate/${platform}@git_revision:d1c03082ecda0148d8096f1fd8bf5491eafc7323',
  'v8/tools/luci-go:infra/tools/luci/isolated/${platform}': 'https://chrome-infra-packages.appspot.com/infra/tools/luci/isolated/${platform}@git_revision:d1c03082ecda0148d8096f1fd8bf5491eafc7323',
  'v8/tools/luci-go:infra/tools/luci/swarming/${platform}': 'https://chrome-infra-packages.appspot.com/infra/tools/luci/swarming/${platform}@git_revision:d1c03082ecda0148d8096f1fd8bf5491eafc7323',
}

以后,就可以在v8的目录下运行gclient sync去同步这些工具。

编译d8

我们是做工具用,所以我们需要的是d8和libv8_monolith.

d8在前面讲字节码的时候介绍过。
我们看下如何编译d8.
以x64 release版为例:

python3 tools/dev/gm.py x64.release

如果要编译debug版,就是

python3 tools/dev/gm.py x64.debug

编译成功之后,就会在out/x64.release或者out/x64.debug下面出现d8.

我们就可以应用./d8 --print-bytecode查看字节码啦。

将v8作为库使用

第一步,我们先生成v8库对应的gn文件:

python3 ./tools/dev/v8gen.py x64.release.sample

在out.gn/x64.release.sample中会生成一堆ninja文件,比如下面是v8_monolith.ninja文件:

defines = -D_LIBCPP_HAS_NO_ALIGNED_ALLOCATION -DCR_XCODE_VERSION=1310 -DCR_CLANG_REVISION=\"llvmorg-14-init-6355-gb2217b36-2\" -D__STDC_CONSTANT_MACROS -D__STDC_FORMAT_MACROS -D_FORTIFY_SOURCE=2 -D__ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES=0 -DNDEBUG -DNVALGRIND -DDYNAMIC_ANNOTATIONS_ENABLED=0 -DV8_TYPED_ARRAY_MAX_SIZE_IN_HEAP=64 -DENABLE_GDB_JIT_INTERFACE -DENABLE_MINOR_MC -DV8_INTL_SUPPORT -DENABLE_HANDLE_ZAPPING -DV8_ATOMIC_OBJECT_FIELD_WRITES -DV8_ATOMIC_MARKING_STATE -DV8_ENABLE_LAZY_SOURCE_POSITIONS -DV8_SHARED_RO_HEAP -DV8_WIN64_UNWINDING_INFO -DV8_ENABLE_REGEXP_INTERPRETER_THREADED_DISPATCH -DV8_SNAPSHOT_COMPRESSION -DV8_SHORT_BUILTIN_CALLS -DV8_ENABLE_SYSTEM_INSTRUMENTATION -DV8_ENABLE_WEBASSEMBLY -DV8_ALLOCATION_FOLDING -DV8_ALLOCATION_SITE_TRACKING -DV8_ADVANCED_BIGINT_ALGORITHMS -DV8_INCLUDE_RECEIVER_IN_ARGC -DV8_COMPRESS_POINTERS -DV8_COMPRESS_POINTERS_IN_SHARED_CAGE -DV8_31BIT_SMIS_ON_64BIT_ARCH -DV8_DEPRECATION_WARNINGS -DV8_IMMINENT_DEPRECATION_WARNINGS -DCPPGC_CAGED_HEAP -DV8_TARGET_ARCH_X64 -DV8_HAVE_TARGET_OS -DV8_TARGET_OS_MACOSX -DV8_RUNTIME_CALL_STATS -DU_USING_ICU_NAMESPACE=0 -DU_ENABLE_DYLOAD=0 -DUSE_CHROMIUM_ICU=1 -DU_ENABLE_TRACING=1 -DU_ENABLE_RESOURCE_TRACING=0 -DU_STATIC_IMPLEMENTATION -DICU_UTIL_DATA_IMPL=ICU_UTIL_DATA_FILE
framework_dirs =
include_dirs = -I../.. -Igen -I../../include -Igen/include -I../../third_party/icu/source/common -I../../third_party/icu/source/i18n
label_name = v8_monolith
target_out_dir = obj
target_output_name = libv8_monolith

build obj/v8_monolith.inputdeps.stamp: stamp obj/generate_bytecode_builtins_list.stamp obj/run_gen-regexp-special-case.stamp obj/run_torque.stamp obj/src/inspector/protocol_generated_sources.stamp obj/third_party/icu/icudata.stamp

也可以直接使用gn命令达到和v8gen.py同样的效果:

gn args out.gn/x64.release.sample

如果看v8gen.py的源码的话我们会发现,基本上都是在操作生成gn的参数。

第二步,我们就可以用ninja来编译libv8_monolith了:

ninja -C out.gn/x64.release.sample v8_monolith

同样,如果想要编译d8,将目标改成d8即可:

ninja -C out.gn/x64.release.sample d8

第三步,编译前面写的使用V8 API的C++程序:

g++ -I. -Iinclude ./hello3.cc -o hello_world3 -lv8_monolith -Lout.gn/x64.release.sample/obj/ -lpthread -std=c++14 -DV8_COMPRESS_POINTERS

小结

本文中我们学习了编译v8和将v8当成库引入到你自己的应用程序中的方法。并且通过简单的例子就实现了执行字符串脚本和从文件中读取js脚本的功能。
这为我们进一步学习v8细节打好了坚实的基础。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容