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