LoginSignup
7

More than 5 years have passed since last update.

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

Last updated at Posted at 2016-12-21

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>します。

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

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解放されてない気がする……?)
次に、おまじないを書いていきます。

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です。

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という変数が参照できるようになります(多分)。

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

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

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つ、かつ文字列なら、その長さを返してみましょう。

    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と受け取ったTestnameを足したnameを持つTestを返す関数を持つ

(多分動くと思う)

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をこちらで用意するという選択を取るのが非常に難しく、厳しいです。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7