將v8變成工具
如何嵌入一個(gè)v8引擎到你的應(yīng)用中
v8除了可以作為一個(gè)獨(dú)立的js引擎之外瓷叫,還可以通過庫的方式嵌入到我們的應(yīng)用中综膀,它以V8 API的方式服務(wù)我們。
我們來看一個(gè)老一點(diǎn)的例子來看看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;
}
官方的例子是輸出一個(gè)"Hello,World"字符串动知。我覺得這完全沒有展示出這個(gè)例子的強(qiáng)大之處咪鲜,因?yàn)槟芷唇幼址莻€(gè)非常常規(guī)的操作,而我們引進(jìn)來的是可以解析js代碼的引擎滋捶。不寫幾條js語句痛悯,真對不起這個(gè)例子。
我們將剛才的例子存為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這個(gè)庫怎么編譯出來的重窟,我們后面講編譯v8源代碼的時(shí)候會講载萌。
然后運(yùn)行生成的hello_world2,輸出:
256
總體流程就是:
- 初始化ICU
- 初始化v8平臺
- 創(chuàng)建分配器
- 創(chuàng)建Isolate
- 創(chuàng)建Scope
- 創(chuàng)建Context
- 創(chuàng)建腳本
- 編譯腳本
- 運(yùn)行腳本
- 清理Isolate
- 清理v8
- 清理分配器
其中亲族,Isolate代表一個(gè)線程不安全的v8運(yùn)行實(shí)例炒考。其它概念大家基本都可以理解。
新一點(diǎn)的API結(jié)構(gòu)上有一些變化霎迫,我們來看下:
#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è)文件名存斋枢,用剛才一樣的參數(shù)去編譯:
g++ -I. -Iinclude ./hello3.cc -o hello_world3 -lv8_monolith -Lout.gn/x64.release.sample/obj/ -lpthread -std=c++14 -DV8_COMPRESS_POINTERS
雖然API變化了,但是都還是被支持的知给。
總體上瓤帚,我們發(fā)現(xiàn)流程上基本沒有太大變化。只是API拆解得更細(xì)了涩赢。原來只要包含一個(gè)v8.h就夠了戈次,現(xiàn)在context, isolate, primitive, script等都拆分成獨(dú)立的API了。
上面的版本是經(jīng)過我加工過的筒扒,其實(shí)還有wasm的部分被我刪除掉了怯邪。在2021年末的這個(gè)時(shí)刻,wasm的知識對于很多前端同學(xué)還不是必備的基礎(chǔ)花墩。后面我們有專文討論wasm基礎(chǔ)加上v8的實(shí)現(xiàn)悬秉。
本地句柄
作為一個(gè)JavaScript引擎澄步,v8自然是擁有一個(gè)運(yùn)行時(shí)的垃圾回收器。垃圾回收器會回收一切沒有句柄持有的對象和泌。
最簡單的句柄叫做本地句柄村缸,它是跟棧綁定的。當(dāng)退出一個(gè)作用域時(shí)武氓,本地局柄所持有的對象也將都被釋放掉梯皿。
本地句柄的用法是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);
針對于可能失敗忠烛,為空或者出現(xiàn)異常的情況百姓,我們可以使用MaybeLocal,例如:v8::MaybeLocal<v8::String>
.
從utf-8字符串構(gòu)造v8 String的v8::String::NewFromUtf8
函數(shù)况木,默認(rèn)生成的就是v8::MaybeLocal<v8::String>
類型垒拢。
從MaybeLocal轉(zhuǎn)換到Local,可以調(diào)用ToLocalChecked函數(shù)火惊。其原型為:
V8_INLINE Local<T> ToLocalChecked() {
if (V8_UNLIKELY(val_ == nullptr)) api_internal::ToLocalEmpty();
return Local<T>(val_);
}
v8字符串
v8::String所表示的就是JavaScript的字符串求类。
上節(jié)我們說了,要生成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字符串轉(zhuǎn)換成C字符串尸疆,需要通過v8::String::Utf8Value類型來轉(zhuǎn)換。
v8::String::Utf8Value v8str2(isolate, v8str1.ToLocalChecked());
printf("%s\n", *v8str2);
我們來看個(gè)將文件讀到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函數(shù)惶岭,就可以改成運(yùn)行文件上的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
除了內(nèi)存管理之外,另外一個(gè)很重要的事情就是運(yùn)行上下文Context. 我們可以看到鸯旁,在代碼中噪矛,編譯需要上下文,運(yùn)行需要上下文铺罢。
我們知道艇挨,js默認(rèn)是提供很多全局變量和全局函數(shù),這些都存儲于上下文中韭赘。腳本中的對象缩滨、函數(shù)也都是跟上下文綁定在一起的。
我們來看一下官方的圖:
編譯v8
下面我們就再說下如何編譯v8.
下載v8源代碼
v8是個(gè)比較復(fù)雜的工程,只從github上下載 https://github.com/v8/v8 的代碼是沒法編譯的脉漏。
我們以mac和Linux為例蛋勺。
首先我們需要下載depot_tools工具包:
git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
然后將這個(gè)工具包加到path中,后面下載所用的fetch, 編譯所用的gn等工具都在這里鸠删。
export PATH=/path/to/depot_tools:$PATH
第三步,我們都過fetch工具下載v8:
fetch v8
如果不想用fetch工具贼陶,想直接手動(dòng)下載的話刃泡,需要下載以下的庫到相應(yīng)的目錄下。
比如我把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的目錄下運(yùn)行gclient sync
去同步這些工具。
編譯d8
我們是做工具用撮胧,所以我們需要的是d8和libv8_monolith.
d8在前面講字節(jié)碼的時(shí)候介紹過桨踪。
我們看下如何編譯d8.
以x64 release版為例:
python3 tools/dev/gm.py x64.release
如果要編譯debug版,就是
python3 tools/dev/gm.py x64.debug
編譯成功之后芹啥,就會在out/x64.release或者out/x64.debug下面出現(xiàn)d8.
我們就可以應(yīng)用./d8 --print-bytecode查看字節(jié)碼啦锻离。
將v8作為庫使用
第一步,我們先生成v8庫對應(yīng)的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命令達(dá)到和v8gen.py同樣的效果:
gn args out.gn/x64.release.sample
如果看v8gen.py的源碼的話我們會發(fā)現(xiàn)汽纠,基本上都是在操作生成gn的參數(shù)。
第二步傀履,我們就可以用ninja來編譯libv8_monolith了:
ninja -C out.gn/x64.release.sample v8_monolith
同樣虱朵,如果想要編譯d8,將目標(biāo)改成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
小結(jié)
本文中我們學(xué)習(xí)了編譯v8和將v8當(dāng)成庫引入到你自己的應(yīng)用程序中的方法碴犬。并且通過簡單的例子就實(shí)現(xiàn)了執(zhí)行字符串腳本和從文件中讀取js腳本的功能。
這為我們進(jìn)一步學(xué)習(xí)v8細(xì)節(jié)打好了堅(jiān)實(shí)的基礎(chǔ)梆暮。