V8: ビルドと C++ への組み込み (作業メモ)

# この記事について

以下のチュートリアル・サンプルを見て JavaScript の V8 エンジンを C++ に組み込んでみた際の作業メモ(初歩的な内容。C++は詳しくないのでコードの書き方が変かも)。

- [Building V8 from source](https://v8.dev/docs/build)
- [Getting started with embedding V8](https://v8.dev/docs/embed)
- [Sample hello-world](https://chromium.googlesource.com/v8/v8/+/refs/heads/master/samples/hello-world.cc)

####使用した環境

- Ubuntu 18.04
- Python2.7
  - ubuntu 18.04 ではデフォルトで入らないのでインストールする

```
sudo apt install python2.7 python-pip
```

- 開発ツール
  - `build-essential` をインストール

```bash
$ 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 の開発で用いられてる開発ツール群。 

```bash
$ 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 のソースを取得する。

```bash
$ cd ~
$ mkdir -p repos/v8
$ cd repos/v8/
$ fetch v8
$ cd v8
```

依存ライブラリ等をインストールする。

```bash
# Download all the build dependencies
$ cd ~/repos/v8/v8
$ gclient sync

# Install additional build dependencies
$ ./build/install-build-deps.sh
```

### V8をスタティックライブラリとしてビルドする

今回は V8 を自前の C++ アプリに組み込みたいので、V8 をスタティックライブラリとしてビルドする。

```bash
# ビルド
$ 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](https://chromium.googlesource.com/v8/v8/+/refs/heads/master/samples/hello-world.cc) にちょっと手を入れただけ。引数で渡された JavaScript ファイルを実行するもの。

```bash
cd ~/repos
mkdir testapp
cd testapp/
vi main.cc
```

以下、main.cc の内容。

```cpp
#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 を作成。

```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 という実行ファイルができる。

```bash
make build
```

### 実行

以下の JavaScript ファイルを用意(sample.js とする)。

```javascript
function test() {
    return "Hello!"
}

test()
```

コンパイルした自前アプリを実行してみる。

```bash
$ ./testapp sample.js 
Hello!
```

v8 の samples ディレクトリには hello-world の他にもサンプルがあるので見てみるとよさそう。

# 補足

## V8 で使われる各種の型、メソッド
以下、v8.h から引用。

### v8::Local
```cpp
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
```cpp
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.