v8を使ったマクロ機能の作り方

# v8とは
v8はGoogleが開発しているJavaScriptエンジンです。
TATEditorのマクロ機能はこのv8を利用したものとなっており、今回はそのお話です。

## 注意
v8はAPIが目まぐるしく変化するため、少し前のコードがすぐにビルドできなくなります。
今回は2016/09/23時点でのmasterの`HEAD`をビルドした際の内容になっています。
このとき半年前のコードが使えなくなっていました。

# ビルド
以下はmacOSやUbuntuでのビルド方法です。
（ただし、DeprecatedなGypを使った方法です。GNを使った上でICUを自前のものに置換する方法が構築できなかったため）

- `git clone https://chromium.googlesource.com/chromium/tools/depot_tools.git`
- `export PATH=`pwd`/depot_tools:"$PATH"`
- `fetch v8`
- `git pull origin`
- `gclient sync`
- `cd ../v8`
- 勝手に入ってきたICUのソースコードを利用しないためにごにょごにょする
- `make native GYPFLAGS="-Dclang=0" CFLAGS="-I/usr/local/include/"`
    - 普通は`make native`でいけるはずです

Windowsはこの2016/09/23時点で、ICUのビルドをv8のビルドと分離する方法が見つけられなかった。

# 基本的な使い方

`#include <v8.h>`します。

便利そうなクラスを用意します。

```cpp
class Macro
{
public:
    static v8::Platform* platform;
    v8::Isolate* isolate;
    class Scope
    {
    public:
        v8::Isolate::Scope isolate_scope;
        v8::HandleScope handle_scope;
        Scope(v8::Isolate* isolate)
            : isolate_scope(isolate)
            , handle_scope(isolate)
        {
        }
    }
    *scope;
    v8::Local<v8::ObjectTemplate> global;
    v8::Local<v8::Context> context;
    Macro(void)
        : isolate(v8::Isolate::New(params()))
        , scope(new Scope(isolate))
        , global(v8::ObjectTemplate::New(isolate))
    {
        init();
        context = v8::Context::New(isolate, NULL, global);
        context->Enter();
    }
    ~Macro(void)
    {
        context->Exit();
        delete scope;
        scope = NULL;
        isolate->Dispose();
        isolate = NULL;
    }
    static v8::Isolate::CreateParams params(void);
    static void Initialize(void);
    static void Uninitialize(void);
    bool proc(const char *source, std::string& result);
    void refresh(void)
    {
        context->Exit();
        context = v8::Context::New(isolate, NULL, global);
        context->Enter();
    }
private:
    void init(void);
    void uninit(void);
};
```

（これcontext解放されてない気がする……？）
次に、おまじないを書いていきます。

```cpp
v8::Platform* Macro::platform = NULL;

class ArrayBufferAllocator : public v8::ArrayBuffer::Allocator
{
public:
    virtual void* Allocate(size_t length)
    {
        void* data = AllocateUninitialized(length);
        return data == NULL ? data : memset(data, 0, length);
    }
    virtual void* AllocateUninitialized(size_t length) { return malloc(length); }
    virtual void Free(void* data, size_t) { free(data); }
};

v8::Isolate::CreateParams Macro::params(void)
{
    ArrayBufferAllocator allocator;
    v8::Isolate::CreateParams create_params;
    create_params.array_buffer_allocator = &allocator;
    return create_params;
}

void Macro::Initialize(void)
{
    if(platform == NULL)
    {
        v8::V8::InitializeICU();
        platform = v8::platform::CreateDefaultPlatform();
        v8::V8::InitializePlatform(platform);
        v8::V8::Initialize();
    }
}

void Macro::Uninitialize(void)
{
    if(platform != NULL)
    {
        v8::V8::Dispose();
        v8::V8::ShutdownPlatform();
        delete platform;
    }
}
```

これにJavaScriptを実行させる関数が`proc`です。

```cpp
bool Macro::proc(const char *source, std::string& result)
{
    if(source == NULL || *source == 0)
        return false;
    auto js = v8::String::NewFromUtf8(isolate, source, v8::NewStringType::kNormal).ToLocalChecked();
    auto script = v8::Script::Compile(context, js);
    if(script.IsEmpty())
        return false;
    auto output = script.ToLocalChecked()->Run(context);
    if(output.IsEmpty())
        return false;
    v8::String::Utf8Value utf8(output.ToLocalChecked());
    result = *utf8;
    return true;
}
```

`proc`は何度も呼べて、呼んだ際には前回までの変数とかが残ってます。
JavaScriptのコードを`const char *`でもらってるのは気にしないでください。

# マクロの実装 

さて、以上でJavaScriptを実行できるクラスができたのですが、足し算や文字列処理だけができても何も嬉しくないので、C++側の関数を呼び出せるようにします。

これを行うのが未実装のまま残ってる`init`です。
ここだけ実装すればあとはJavaScriptを実行するだけです。

まず、次のように定義して置くと、JavaScript上から`utility`という変数が参照できるようになります（多分）。

```cpp
void Macro::init(void)
{
    auto utility = v8::ObjectTemplate::New(isolate);
    global->Set(v8::String::NewFromUtf8(isolate, "utility"), utility);
}
```

ここに以下のように1行足すと`utility.test`という関数ができます。
この関数には、引数をいくつ渡しても`test\n`を吐き出すだけです。
返り値はありません。

```cpp
void Macro::init(void)
{
    auto utility = v8::ObjectTemplate::New(isolate);
    utility->Set(v8::String::NewFromUtf8(isolate, "test"), v8::FunctionTemplate::New(isolate, Macro::test));
    global->Set(v8::String::NewFromUtf8(isolate, "utility"), utility);
}

class Macro
{
    ...

    static void test(const v8::FunctionCallbackInfo<v8::Value>& args)
    {
        printf("test\n");
    }

    ...
}
```

もう少し複雑にして、引数が1つ、かつ文字列なら、その長さを返してみましょう。

```cpp
    static void test(const v8::FunctionCallbackInfo<v8::Value>& args)
    {
        v8::Isolate* isolate = args.GetIsolate();
        if(args.Length() != 1 || !args[0]->IsString())
        {
            isolate->ThrowException(v8::Exception::TypeError(v8::String::NewFromUtf8(isolate, "Invalid arguments.")));
        }
        v8::String::Utf8Value utf8(args[i]->ToString());
        std::string value = *utf8;
        auto length = value.length();
        args.GetReturnValue().Set(length);
    }
```

ちなみに文字列を返すときは`args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, "abcdef"));`とかです。

## C++内のオブジェクトをやり取りする
`Test`というクラスを実装しています。

- JavaScriptから`name`という文字列フィールドにアクセスできる
- `merge`という`Test`を受け取って自身の`name`と受け取った`Test`の`name`を足した`name`を持つ`Test`を返す関数を持つ

（多分動くと思う）

```cpp
template<class T>
void destroyer(const v8::WeakCallbackInfo<T> &data)
{
    auto p = data.GetParameter();
    p->second->Reset();
    delete p->first;
    delete p;
}

template<class T>
inline T* Cast(const v8::FunctionCallbackInfo<v8::Value>& args)
{
    return static_cast<T*>(v8::Local<v8::External>::Cast(args.Holder()->GetInternalField(0))->Value());
}

class Test
{
public:
    Test(const std::string& name)
        : name(name)
    {
    }
    Test(const Test& obj)
        : name(obj.name)
    {
    }

    static v8::Local<v8::Object> Create(v8::Isolate* isolate, Test *value)
    {
        auto clazz = v8::ObjectTemplate::New(isolate);

        clazz->SetAccessor(v8::String::NewFromUtf8(isolate, "name"), Test::getName, Test::setName);
        clazz->Set(v8::String::NewFromUtf8(isolate, "merge"), v8::FunctionTemplate::New(isolate, Test::merge));

        clazz->SetInternalFieldCount(1);
        auto obj = clazz->NewInstance();
        obj->SetInternalField(0, v8::External::New(isolate, value));
        v8::Persistent<v8::Object> *holder = new v8::Persistent<v8::Object>(isolate, obj);
        typedef std::pair<Test *, v8::Persistent<v8::Object> *> Destroyer;
        holder->SetWeak<Destroyer>(new Destroyer(value, holder), destroyer<Destroyer>, v8::WeakCallbackType::kParameter);
        return obj;
    }

    std::string name;
    static void getName(v8::Local<v8::String> property, const v8::PropertyCallbackInfo<v8::Value>& info)
    {
        v8::Local<v8::Object> self = info.Holder();
        v8::Local<v8::External> wrap = v8::Local<v8::External>::Cast(self->GetInternalField(0));
        void* ptr = wrap->Value();
        info.GetReturnValue().Set(v8::String::NewFromUtf8(info.GetIsolate(), static_cast<Test*>(ptr)->name.c_str()));
    }
    static void setName(v8::Local<v8::String> property, v8::Local<v8::Value> value, const v8::PropertyCallbackInfo<void>& info)
    {
        v8::Local<v8::Object> self = info.Holder();
        v8::Local<v8::External> wrap = v8::Local<v8::External>::Cast(self->GetInternalField(0));
        void* ptr = wrap->Value();
        v8::String::Utf8Value utf8(value);
        if(utf8.length() > 0)
            static_cast<Test*>(ptr)->name = *utf8;
        else
            static_cast<Test*>(ptr)->name = "";
    }

    static void merge(const v8::FunctionCallbackInfo<v8::Value>& args)
    {
        v8::Isolate* isolate = args.GetIsolate();
        Test *self = Cast<Test>(args);
        if(args[0]->IsObject())
        {
            auto obj = args[0]->ToObject();
            if(obj->InternalFieldCount() == 1)
            {
                auto test = static_cast<Test *>(v8::Local<v8::External>::Cast(obj->GetInternalField(0))->Value());
                auto name = self->name + test->name;
                args.GetReturnValue().Set(Create(isolate, new Test(name)));
                return;
            }
        }
        isolate->ThrowException(v8::Exception::TypeError(v8::String::NewFromUtf8(isolate, "Invalid arguments.")));
    }
};
```

これで、関数で呼び出したC++オブジェクトを参照できて、引数のC++オブジェクトを参照できて、新しいC++オブジェクトを返せるようになりました。
もうあとはなんでもできそうですね。
（JavaScriptがよくわからないのでJavaScript的にどうなってるのかはよくわからない）

# おわりに
実際のソースコードでは`#define`を駆使して関数の登録や、変数の登録、インスタンスの生成などを簡略化しています。

ちなみに次にマクロ機能を整理する際にはv8からChakraCoreに乗り換えようと思っています。
ChakraCoreはMicrosoft Edgeに使われているJavaScriptエンジンで、ビルドしやすいです。
ビルドがしやすいので移行します（大事なことなので2回言いました）。
v8のビルドでICUをこちらで用意するという選択を取るのが非常に難しく、厳しいです。