読者です 読者をやめる 読者になる 読者になる

凹みTips

C++、JavaScript、Unity、ガジェット等の Tips について雑多に書いています。

v8 を利用した C++ から JavaScript へクラスを簡単にエクスポートするヤツ作ってみた

C++ JavaScript V8

はじめに

前回、v8 を利用した C++ から JavaScript 側へ簡単に関数をエクスポートして実行するクラスを作ってみた - 凹みTips というエントリを書いたのですが、今回はクラスも簡単にエクスポートできるようにしてみました。前回の内容も踏まえて紹介します。

環境

  • Ubuntu 10.04 LTS
  • g++-4.6 (GCC) 4.6.1 20110617 (prerelease)

使用例

インラインで実行

前回の差分として、インラインでも実行できるようにしておきました。

#include <iostream>
#include "v8.hpp"

int main(int argc, char const* argv[])
{
	hecomi::V8::JavaScript js;
	js.set("print", [](const v8::Arguments& args)->v8::Handle<v8::Value> {
		v8::String::Utf8Value str(args[0]);
		std::cout << *str << std::endl;
		return v8::Undefined();
	});
	js.create_context();
	js.exec("print('(」・ω・)」うー!(/・ω・)/にゃー!');");
	return 0;
}

出力:

(」・ω・)」うー!(/・ω・)/にゃー!
ファイル実行

前回のもそのまま使えます。

#include <iostream>
#include "v8.hpp"

int main(int argc, char const* argv[])
{
	hecomi::V8::JavaScript js;
	js.set("print", [](const v8::Arguments& args)->v8::Handle<v8::Value> {
		v8::String::Utf8Value str(args[0]);
		std::cout << *str << std::endl;
		return v8::Undefined();
	});
	js.create_context();
	js.exec_script("unya.js");
	return 0;
}

unya.js :

print((function(){
	return function(){
		return function(){
			return "(」・ω・)」うー!(/・ω・)/にゃー!";
		}
	}
})()()());

出力 :

(」・ω・)」うー!(/・ω・)/にゃー!
クラスをエクスポート
#include <iostream>
#include <utility>
#include "v8.hpp"

class Hoge : public hecomi::V8::ExportToJSIF
{
private:
	boost::any a_, b_, c_, d_;

public:
	boost::any get(const std::string& property_name)
	{
		if      (property_name == "a") return a_;
		else if (property_name == "b") return b_;
		else if (property_name == "c") return c_;
		else if (property_name == "d") return d_;
		else std::cerr << "Error! Hoge does not have " << property_name << std::endl;
	}

	template <class T>
	void set(const std::string& property_name, T value)
	{
		if      (property_name == "a") a_ = value;
		else if (property_name == "b") b_ = value;
		else if (property_name == "c") c_ = value;
		else if (property_name == "d") d_ = value;
		else std::cerr << "Error! Hoge does not have " << property_name << std::endl;
	}

	boost::any func(const std::string& func_name, const v8::Arguments& args)
	{
		if (func_name == "swap") {
			std::swap(c_, d_);
			return 0;
		}
		else if (func_name == "multiple_a_by") {
			int a = boost::any_cast<int>(a_);
			int x = args[0]->Int32Value();
			return a*x;
		}
	}
};

int main(int argc, char const* argv[])
{
	hecomi::V8::JavaScript js;
	js.set("print", [](const v8::Arguments& args)->v8::Handle<v8::Value> {
		v8::String::Utf8Value str(args[0]);
		std::cout << *str << std::endl;
		return v8::Undefined();
	});

	hecomi::V8::ExportToJS<Hoge> hoge("Hoge");
	hoge.add_var<int>("a");
	hoge.add_var<double>("b");
	hoge.add_var<std::string>("c");
	hoge.add_var<std::string>("d");
	hoge.add_func("swap");
	hoge.add_func<int>("multiple_a_by");
	js.set(hoge);

	js.create_context();
	js.exec_script("hoge.js");
	return 0;
}

hoge.js :

var hoge = new Hoge();

// 値セット
hoge.a = 10;
hoge.b = 1.234;
hoge.c = "this is c";
hoge.d = "this is d";

// 見てみる
print(hoge.a);
print(hoge.b);
print(hoge.c);
print(hoge.d);

// c と d 交換されるかな?
hoge.swap();
print(hoge.c);
print(hoge.d);

// aに数字をかけてみる
print(hoge.multiple_a_by(10));

// 違う型を代入してみる
hoge.a = "10";
hoge.c = 10;
print(hoge.a);
print(hoge.c);

出力 :

10
1.234
this is c
this is d
this is d
this is c
100
Error! String was subsituted into [object Hoge].a, but it was wrong type.
Error! Integer was subsituted into [object Hoge].c, but it was wrong type.
10
this is d

hecomi::V8::ExportToJSIF を継承したクラスを作ってその中で色々と必要な処理を書きます。そして、ExportToJS のテンプレート引数としてこのクラスを指定して、add_var でメンバ変数を追加したり、add_func でメンバ関数を追加したりした後、hecomi::v8::JavaScript::set でそのインスタンスを突っ込めばエクスポート完了です。
JavaScript へエクスポートするメンバ変数は boost::any にしてください。これで set/get が1箇所で書けるようになります。メンバ変数は ExportToJS::add_var を使って何の型か JavaScript 側へ教え込みます。何でも代入が許されてしまう JavaScript 側ですが、こうして型に制約を持たせることで、JavaScript 側から返ってきた値を安全に元の型へキャストして使用できるようになります。間違った方が代入された場合は何も起きません。ただし、上記の例にようにかける型は、int(Integer)、double(Number)、std::string(String)に限られています。その他の型については手動で書く必要があります。これについては後ほど紹介します。なお、テンプレート引数を省略すると T = std::string になります。
メンバ関数も同様に戻り値の型を ExportToJS::add_func を使って教え込みます。テンプレート引数を省略すると T = void となって、JavaScript 側では undefined が返ってくるようになります。
ちなみに、変数は Object のプロパティとして、関数は Object.prototype のプロパティとして登録するようにしてます。どっちとも出来るようにしようかとも思いましたが、特に困っていないのでこれで。。

その他の型について

int, double, std::string 以外の場合は面倒ですが以下のように書きます。

#include <iostream>
#include <cmath>
#include "v8.hpp"

struct Point
{
	double x, y;
};

class Hoge : public hecomi::V8::ExportToJSIF
{
private:
	boost::any p_;

public:
	v8::Handle<v8::Value> custom_get(const std::string& property_name)
	{
		if (property_name == "p") {
			Point p = boost::any_cast<Point>(p_);
			v8::Local<v8::Object> obj = v8::Object::New();
			obj->Set(v8::String::New("x"), v8::Number::New(p.x));
			obj->Set(v8::String::New("y"), v8::Number::New(p.y));
			return obj;
		}
	}

	void custom_set(const std::string& property_name, v8::Handle<v8::Value> value)
	{
		if (property_name == "p") {
			v8::Local<v8::Object> obj = value->ToObject();
			double x = obj->Get(v8::String::New("x"))->NumberValue();
			double y = obj->Get(v8::String::New("y"))->NumberValue();
			p_ = Point{x, y};
		}
		else std::cerr << "Error! Hoge does not have " << property_name << std::endl;
	}

	boost::any func(const std::string& func_name, const v8::Arguments& args)
	{
		if (func_name == "print") {
			Point p = boost::any_cast<Point>(p_);
			std::cout << "{x: " << p.x << "; y: " << p.y << "}" << std::endl;
		}
		return 0;
	}
};

int main(int argc, char const* argv[])
{
	hecomi::V8::JavaScript js;
	hecomi::V8::ExportToJS<Hoge> hoge("Point");
	hoge.add_var("p");
	hoge.add_func("print");
	js.set(hoge);

	js.create_context();
	js.exec("var point = new Point()");
	js.exec("point.p = {x: 4, y: 20}");
	js.exec("point.print()");

	return 0;
}

出力 :

{x: 4; y: 20}

int, double, std::string 以外の型については、custom_get/custom_set が呼ばれるようになっていますので、こちらの中身をゴリゴリ書いて下さい。v8 な感じはまったく隠蔽されていないですが、普通に書くよりは大分楽になった気はします。

コンパイル

上記コードはすべて下記のコマンドでコンパイルしています。

g++-4.6 main.cpp -std=c++0x -I./v8/include -L./v8 -lv8 -lpthread

v8 はワークしているディレクトリ直下に置いてコンパイルしておいて下さい。

v8.hpp

こんなんになりました。

#ifndef INCLUDE_V8_HPP
#define INCLUDE_V8_HPP

#include <v8.h>
#include <string>
#include <fstream>
#include <boost/format.hpp>
#include <boost/any.hpp>


#include <boost/type_traits/is_same.hpp>

namespace hecomi {
	namespace V8 {

// プロトタイプ宣言
template <class T> class ExportToJS;

/* ------------------------------------------------------------------------- */
//  class JavaScript
/* ------------------------------------------------------------------------- */
/**
 * v8 で JavaScript を実行するクラス
 */
class JavaScript
{
public:
	/**
	 * コンストラクタ
	 */
	JavaScript() : global_(v8::ObjectTemplate::New())
	{
	}

	/**
	 * デストラクタ
	 * v8::Persistent を片付ける
	 */
	~JavaScript()
	{
		global_.Dispose();
		context_.Dispose();
	}

	/**
	 * JavaScript側へ C++ の関数をエクスポートする
	 * @param[in] func_name 関数名
	 * @param[in] func エクスポートする関数
	 */
	template <class T>
	void set(const std::string& func_name, T func)
	{
		global_->Set(
			v8::String::New(func_name.c_str()),
			v8::FunctionTemplate::New(func)
		);
	}

	/**
	 * JavaScript側へ C++ の関数をエクスポートする
	 * @param[in] clazz エクスポートするクラス
	 */
	template <class T>
	void set(ExportToJS<T> clazz)
	{
		global_->Set(
			v8::String::New(clazz.get_class_name().c_str()),
			clazz.get_class()
		);
	}

	/**
	 * context を明示的に作成
	 */
	void create_context()
	{
		context_ = v8::Context::New(NULL, global_);
	}

	/**
	 *  JavaScript を文字列から実行する
	 *  @parami[in] code JavaScript のソースコード
	 *  @return true: 実行成功, false: 実行失敗
	 */
	bool exec(const std::string& code)
	{
		v8::TryCatch try_catch;

		v8::Handle<v8::String> script = v8::String::New(code.c_str());

		// compile
		v8::Context::Scope context_scope(context_);
		v8::Handle<v8::Script> compiled_script = v8::Script::Compile(script);
		if (compiled_script.IsEmpty()) {
			v8::String::Utf8Value error(try_catch.Exception());
			std::cerr << *error << std::endl;
			return false;
		}

		// run
		v8::Handle<v8::Value> result = compiled_script->Run();
		if (result.IsEmpty()) {
			v8::String::Utf8Value error(try_catch.Exception());
			std::cerr << *error << std::endl;
			return false;
		}

		return true;
	}

	/**
	 *  JavaScript をファイルから実行する
	 *  @parami[in] file_name ファイル名
	 *  @return true: 実行成功, false: 実行失敗
	 */
	bool exec_script(const std::string& file_name)
	{
		v8::TryCatch try_catch;

		// read js file
		std::ifstream ifs(file_name);
		if (ifs.fail()) {
			std::cerr << boost::format("js file open error: %1%\n") % file_name;
			return false;
		}
		std::string buf, code = "";
		while (std::getline(ifs, buf)) {
			code += buf.c_str();
		}

		return exec(code);
	}

private:
	//! Handle scope
	v8::HandleScope handle_scope_;

	//! Global object
	v8::Persistent<v8::ObjectTemplate> global_;

	//! Context
	v8::Persistent<v8::Context> context_;
};

/* ------------------------------------------------------------------------- */
//  class ExportToJS
/* ------------------------------------------------------------------------- */
/**
 * v8 へクラスを簡単にエクスポートするためのクラス
 */
template <class ExportClass>
class ExportToJS
{
public:
	/**
	 *  コンストラクタ
	 *  @parami[in] class_name 登録するクラス名
	 */
	explicit ExportToJS(const std::string& class_name)
	: class_name_(class_name)
	{
		class_ = v8::FunctionTemplate::New(ExportToJS::construct);
		class_->SetClassName(v8::String::New(class_name.c_str()));
		instance_tmpl_ = class_->InstanceTemplate();
		instance_tmpl_->SetInternalFieldCount(1);
		prototype_tmpl_ = class_->PrototypeTemplate();
	}

	/**
	 *  デストラクタ
	 */
	~ExportToJS()
	{
	}

	/**
	 *  生成した FunctionTemplate を取得
	 *  (class JavaScript で利用)
	 *  @return 色々セットした FunctionTemplate
	 */
	v8::Local<v8::FunctionTemplate> get_class() const
	{
		return class_;
	}

	/**
	 *  登録したクラス名を取得
	 *  @return クラス名
	 */
	std::string get_class_name() const
	{
		return class_name_;
	}

	/**
	 *  プロパティを追加
	 *  @param[in] property_name 追加するプロパティ名
	 */
	template <class T = std::string>
	void add_var(const std::string& property_name)
	{
		instance_tmpl_->SetAccessor(
			v8::String::New(property_name.c_str()),
			ExportToJS::get<T>,
			ExportToJS::set<T>
		);
	}

	/**
	 *  メソッドを追加
	 *  @param[in] property_name 追加するメソッド名
	 */
	template <class T = void>
	void add_func(const std::string& property_name)
	{
		prototype_tmpl_->Set(
			v8::String::New(property_name.c_str()),
			v8::FunctionTemplate::New(ExportToJS::func<T>)
		);
	}

private:
	/**
	 *  JavaScript 側でインスタンス生成時に呼ばれる関数
	 *  @param[in] args JavaScript 側で与えられた引数
	 *  @return JavaScript側へ引き渡すオブジェクト
	 */
	static v8::Handle<v8::Value> construct(const v8::Arguments& args)
	{
		ExportClass* instance = new ExportClass();
		v8::Local<v8::Object> this_obj = args.This();
		this_obj->SetInternalField(0, v8::External::New(instance));
		v8::Persistent<v8::Object> holder = v8::Persistent<v8::Object>::New(this_obj);
		holder.MakeWeak(instance, ExportToJS::destruct);
		return this_obj;
	}

	/**
	 *  JavaScript 側で GC がはたらいた時に呼ばれる関数
	 *  @param[in] handle オブジェクトハンドル
	 *  @param[in] parameter MakeWeak の第1引数
	 */
	static void destruct(v8::Persistent<v8::Value> handle, void* parameter)
	{
		ExportClass* instance = static_cast<ExportClass*>(parameter);
		delete instance;
		handle.Dispose();
	}

	/**
	 *  JavaScript 側で変数の値を読み出すときに呼ばれる関数
	 *
	 *  C++ 側にプロパティ名から値を問い合わせて v8::Value にして返す
	 *  @param[in] property_name プロパティ名
	 *  @param[in] info オブジェクトとか入ってるよ
	 *  @return
	 */
	template <class T>
	static v8::Handle<v8::Value> get(v8::Local<v8::String> property_name, const v8::AccessorInfo& info)
	{
		ExportClass* instance = static_cast<ExportClass*>(
			v8::Local<v8::External>::Cast(info.Holder()->GetInternalField(0))->Value()
		);
		v8::String::Utf8Value utf8_property_name(property_name);

		// 型で戻り値を変える
		if (boost::is_same<T, int>::value) {
			return v8::Integer::New(boost::any_cast<int>(instance->get(*utf8_property_name)));
		}
		else if (boost::is_same<T, double>::value) {
			return v8::Number::New(boost::any_cast<double>(instance->get(*utf8_property_name)));
		}
		else if (boost::is_same<T, std::string>::value) {
			return v8::String::New(boost::any_cast<std::string>(instance->get(*utf8_property_name)).c_str());
		}
		else {
			return instance->custom_get(*utf8_property_name);
		}
	}

	/**
	 *  JavaScript 側で変数に値をセットするときに呼ばれる関数
	 *
	 *  C++ 側へプロパティ名から値をセットする
	 *  @param[in] property_name プロパティ名
	 *  @param[in] value セットする値
	 */
	template <class T>
	static void set(v8::Local<v8::String> property_name, v8::Local<v8::Value> value, const v8::AccessorInfo& info)
	{
		ExportClass* instance = static_cast<ExportClass*>(
			v8::Local<v8::External>::Cast(info.Holder()->GetInternalField(0))->Value()
		);
		v8::String::Utf8Value utf8_class_name(info.Holder()->ToString());
		v8::String::Utf8Value utf8_property_name(property_name);
		if (value->IsInt32()) {
			if (!boost::is_same<T, int>::value) {
				std::cerr << boost::format("Error! Integer was subsituted into %1%.%2%, but it was wrong type.\n")
					% *utf8_class_name
					% *utf8_property_name;
				return;
			}
			instance->set(*utf8_property_name, value->Int32Value());
		}
		else if (value->IsNumber()) {
			if (!boost::is_same<T, double>::value) {
				std::cerr << boost::format("Error! Number was subsituted into %1%.%2%, but it was wrong type.\n")
					% *utf8_class_name
					% *utf8_property_name;
				return;
			}
			instance->set(*utf8_property_name, value->NumberValue());
		}
		else if (value->IsString()) {
			if (!boost::is_same<T, std::string>::value) {
				std::cerr << boost::format("Error! String was subsituted into %1%.%2%, but it was wrong type.\n")
					% *utf8_class_name
					% *utf8_property_name;
				return;
			}
			v8::String::Utf8Value utf8_str(value);
			instance->set(*utf8_property_name, std::string(*utf8_str));
		}
		else {
			instance->custom_set(*utf8_property_name, value);
		}
	}

	/**
	 *  メンバ関数を追加する
	 *  @param[in] args JavaScript側から渡される引数
	 *  @return 関数を実行した戻り値
	 */
	template <class T>
	static v8::Handle<v8::Value> func(const v8::Arguments& args)
	{
		ExportClass* instance = static_cast<ExportClass*>(
			v8::Local<v8::External>::Cast(args.This()->GetInternalField(0))->Value()
		);
		v8::String::Utf8Value utf8_func_name(args.Callee()->GetName());

		if (boost::is_same<T, void>::value) {
			instance->func(*utf8_func_name, args);
			return v8::Undefined();
		}
		else if (boost::is_same<T, int>::value) {
			int result = boost::any_cast<int>(instance->func(*utf8_func_name, args));
			return v8::Integer::New(result);
		}
		else if (boost::is_same<T, double>::value) {
			double result = boost::any_cast<double>(instance->func(*utf8_func_name, args));
			return v8::Number::New(result);
		}
		else if (boost::is_same<T, std::string>::value) {
			std::string result = boost::any_cast<std::string>(instance->func(*utf8_func_name, args));
			return v8::String::New(result.c_str());
		}
		else {
			v8::Handle<v8::Value> result = instance->custom_func(*utf8_func_name, args);
			return result;
		}
	}

	const std::string class_name_;
	v8::Local<v8::FunctionTemplate> class_;
	v8::Local<v8::ObjectTemplate> instance_tmpl_;
	v8::Local<v8::ObjectTemplate> prototype_tmpl_;
};

/* ------------------------------------------------------------------------- */
//  class ExportToJSIF
/* ------------------------------------------------------------------------- */
/**
 * v8 へエクスポートするクラスのひな形
 */
class ExportToJSIF
{
public:
	//! メンバ変数の値を取得 (int, double, std::string 版)
	boost::any get(const std::string& property_name)
	{
		std::cerr << "Error! ExportTOJSIF::get is not overrided." << std::endl;
		return 0;
	}

	//! メンバ変数の値を取得 (その他の型版)
	v8::Handle<v8::Value> custom_get(const std::string& property_name)
	{
		std::cerr << "Error! ExportTOJSIF::custom_get is not overrided." << std::endl;
		return v8::Undefined();
	}

	//! メンバ変数に値をセット (int, double, std::string 版)
	template <class T>
	void set(const std::string& property_name, T value)
	{
		std::cerr << "Error! ExportTOJSIF::set is not overrided." << std::endl;
	}

	//! メンバ変数に値をセット (その他の型版)
	void custom_set(const std::string& property_name, v8::Handle<v8::Value> value)
	{
		std::cerr << "Error! ExportTOJSIF::custom_set is not overrided." << std::endl;
	}

	//! 関数を実行 (int, double, std::string 版)
	boost::any func(const std::string& func_name, const v8::Arguments& args)
	{
		std::cerr << "Error! ExportTOJSIF::func is not overrided." << std::endl;
		return 0;
	}

	//! 関数を実行 (int, double, std::string 版)
	v8::Handle<v8::Value> custom_func(const std::string& func_name, const v8::Arguments& args)
	{
		std::cerr << "Error! ExportTOJSIF::custom_func is not overrided." << std::endl;
		return v8::Undefined();
	}
};

	} // namespace V8
} // namespace hecomi

#endif // INCLUDE_V8_HPP

メンバ変数やメンバ関数の型情報は、v8::ObjectTemplate::SetAccessor や v8::ObjectTemplate::Set する際に、プロパティ名と紐付いて v8 側に保存されます。なのであるプロパティの getter/setter が v8 から呼ばれた際は、その getter/setter は自分自身の型を知っているという寸法です。後は型を boost::is_same を使って見てあげて、異なっていたらエラー、合っていたら boost::any_cast したりすればよしなにやってくれちゃう、という感じになっています。
イケてないところもたくさんあると思うので、こうしたら良いんじゃね?とかこうしちゃいました、とか、(」・ω・)」誰か!(/・ω・)/教えて!

おわりに

次からはこれをホームオートメーションシステムに組み込んでいきたいと思います。