この記事について
以下のチュートリアル・サンプルを見て JavaScript の V8 エンジンを C++ に組み込んでみた際の作業メモ(初歩的な内容。C++は詳しくないのでコードの書き方が変かも)。
使用した環境
- Ubuntu 18.04
- Python2.7
- ubuntu 18.04 ではデフォルトで入らないのでインストールする
sudo apt install python2.7 python-pip
- 開発ツール
-
build-essential
をインストール
-
$ sudo apt install build-essential
$ g++ --version
g++ (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
作業後に出来るディレクトリ構成
/home/kitauji/repos/
├── testapp
│ ├── main.cc
│ ├── sample.js
│ └── testapp
└── v8
└── v8
├── include
├── samples
├── src
├── などなど多数
手順
V8用の開発環境構築
V8 のソースコードを取得
まず、depot_tools をインストールする。depot_tools は Chromium の開発で用いられてる開発ツール群。
$ cd ~/bin
$ git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git
# 上記 depot_tools ディレクトリへの PATH を追加する。その後 .profile を再読み込みする
# (例) PATH="$HOME/bin/depot_tools:$PATH"
$ vi ~/.profile
# depot_tools の更新
$ gclient
※ なお、gclient はこんな感じの出力になったが、これでいいのか分からない。
WARNING: Your metrics.cfg file was invalid or nonexistent. A new one will be created.
Usage: gclient.py <command> [options]
Meta checkout dependency manager for Git.
...
fetch で V8 のソースを取得する。
$ cd ~
$ mkdir -p repos/v8
$ cd repos/v8/
$ fetch v8
$ cd v8
依存ライブラリ等をインストールする。
# Download all the build dependencies
$ cd ~/repos/v8/v8
$ gclient sync
# Install additional build dependencies
$ ./build/install-build-deps.sh
V8をスタティックライブラリとしてビルドする
今回は V8 を自前の C++ アプリに組み込みたいので、V8 をスタティックライブラリとしてビルドする。
# ビルド
$ cd ~/repos/v8/v8
$ tools/dev/v8gen.py x64.release.sample
$ ninja -C out.gn/x64.release.sample v8_monolith
# 生成されたライブラリ(libv8_monolith.a)を確認
$ file out.gn/x64.release.sample/obj/libv8_monolith.a
out.gn/x64.release.sample/obj/libv8_monolith.a: current ar archive
アプリへの V8 の組み込み
基本概念
最初に基本的な概念が分からないとコードを見てもさっぱりなので、以下に記載(https://v8.dev/docs/embed より引用):
- An isolate is a VM instance with its own heap.
- A local handle is a pointer to an object. All V8 objects are accessed using handles. They are necessary because of the way the V8 garbage collector works.
- A handle scope can be thought of as a container for any number of handles. When you've finished with your handles, instead of deleting each one individually you can simply delete their scope.
- A context is an execution environment that allows separate, unrelated, JavaScript code to run in a single instance of V8. You must explicitly specify the context in which you want any JavaScript code to be run.
自前アプリのコード記述
以下は V8のサンプルの hello-world.cc にちょっと手を入れただけ。引数で渡された JavaScript ファイルを実行するもの。
cd ~/repos
mkdir testapp
cd testapp/
vi main.cc
以下、main.cc の内容。
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include "include/libplatform/libplatform.h"
#include "include/v8.h"
void Usage() {
std::cout << "testapp <JavaScript FilePath>" << std::endl;
}
void ReadFile(const char* filePath, std::string& contents) {
std::ifstream f(filePath);
std::stringstream buffer;
buffer << f.rdbuf();
contents = buffer.str();
}
int main(int argc, char* argv[]) {
if ( argc < 2 ) {
Usage();
return 1;
}
// Read a JavaScript file contents.
char* filePath = argv[1];
std::string fileContents;
ReadFile(filePath, fileContents);
// Initialize V8.
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();
// Create a new Isolate
v8::Isolate::CreateParams isolateCreateParam;
isolateCreateParam.array_buffer_allocator = v8::ArrayBuffer::Allocator::NewDefaultAllocator();
v8::Isolate* isolate = v8::Isolate::New(isolateCreateParam);
{
v8::Isolate::Scope isolateScope(isolate);
// Create a stack-allocated handle scope.
v8::HandleScope handleScope(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 contextScope(context);
{
// Create a string containing the JavaScript source code.
v8::Local<v8::String> source = v8::String::NewFromUtf8(isolate, fileContents.c_str(), 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();
v8::String::Utf8Value utf8(isolate, result);
std::cout << *utf8 << std::endl;
}
}
// Dispose the isolate and tear down V8.
isolate->Dispose();
v8::V8::Dispose();
v8::V8::ShutdownPlatform();
delete isolateCreateParam.array_buffer_allocator;
return 0;
}
ビルド
以下のような Makefile を作成。
INCLUDE = -I../v8/v8 -I../v8/v8/include
CXX = g++
CFLAGS = -pthread -std=c++0x -DV8_COMPRESS_POINTERS
LDFLAGS = -L../v8/v8/out.gn/x64.release.sample/obj/
LIBS = -lv8_monolith
build:
$(CXX) $(INCLUDE) main.cc -o hello $(LDFLAGS) $(LIBS) $(CFLAGS)
そしてビルドを実行。同じディレクトリに hello という実行ファイルができる。
make build
実行
以下の JavaScript ファイルを用意(sample.js とする)。
function test() {
return "Hello!"
}
test()
コンパイルした自前アプリを実行してみる。
$ ./testapp sample.js
Hello!
v8 の samples ディレクトリには hello-world の他にもサンプルがあるので見てみるとよさそう。
補足
V8 で使われる各種の型、メソッド
以下、v8.h から引用。
v8::Local
template <class T>
class Local
An object reference managed by the v8 garbage collector.
All objects returned from v8 have to be tracked by the garbage collector so that it knows that the objects are still alive. Also, because the garbage collector may move objects, it is unsafe to point directly to an object. Instead, all objects are stored in handles which are known by the garbage collector and updated whenever an object moves. Handles should always be passed by value (except in cases like out-parameters) and they should never be allocated on the heap.
v8::MaybeLocal
template<class T>
class v8::MaybeLocal
A MaybeLocal<> is a wrapper around Local<> that enforces a check whether the Local<> is empty before it can be used.
-
v8::Local<T> ToLocalChecked()
Converts this MaybeLocal<> to a Local<>. If this MaybeLocal<> is empty, V8 will crash the process. -
template <class S> bool ToLocal(Local<S>* out) const
Converts this MaybeLocal<> to a Local<>. If this MaybeLocal<> is empty, |false| is returned and |out| is left untouched.