これは Siv3D Advent Calendar 2016 - 20 日目の記事です。
**「 Siv3D はいいぞ!」**というセリフをここ数ヶ月で何回もききました。
これは使ってみるしかない! ということで、開発環境を整えていたその時!
JavaScript 以外の言語を使うと死んでしまう病 に感染してしまったのです。
Siv3D を使ってみたい... でも JavaScript 以外を使うと死んでしまう...
ん? それなら Siv3D で JavaScript を使えばいい のでは!!!?!??!?
JavaScript ってなんぞや?
アレです。 Java ではないです。
この記事をウェブブラウザで読んでいただいているなら、ここでも JavaScript は働いています。
この記事をウェブブラウザ以外で読んでいる特殊な方は見なかったことにしてください。
JavaScript は名前に入っている通りのスクリプト言語なので、それを実行するインタプリタ(いわゆる JavaScript エンジン)が存在します。
色々な種類のウェブブラウザがありますが( Internet Explorer ? なにそれおいしいの? )JavaScript エンジンもいくつかあるみたいです。
もしかしたら、 Siv3D に組み込める JavaScript エンジン があるかもしれません。
選ばれたのは、 V8 でした。
Google V8 JavaScript Engine は、 Google が開発しているオープンソースの JavaScript エンジンです。
Google が関わってるということで、 Chrome や Android Browser などのウェブブラウザに使われています。
その他にも、今時の JavaScript 開発に必要不可欠な Node.js は V8 で動作しています。
V8 の Wikipedia を読んでみたら、次のような記述がありました。
wikipedia
C++ で記述されている。スタンドアロンでの実行が可能なほか、 C++ で書かれたアプリケーションの一部として動作させることもできる。
こ、これは...!?
V8 をビルドする
※ この章は自分で V8 をビルドしたい方以外は飛ばして OK です。
V8 エンジンを使うには、 V8 のプロジェクトを持ってきてビルドする必要があります。
V8 をビルドする流れを解説している日本語の記事はいくつかあったのですが、 V8 のバージョンによって方法が違うようです。
VisualStudio 2015 で V8 - 5.6.0 (candidate) をビルドした時に参考にしたサイト様を載せておきます。
上記のサイトを参考に作業を進めると all.sln
が生成されるので、あとはそれを VisualStudio で開いてビルドするだけです。
ビルドに成功すると、 V8 を使う際に必要な DLL や、スナップショットという仕組みに必要な追加ファイルが生成されます。
ちなみに、ビルド時のオプションでスナップショットの追加ファイルを使わない設定にできるそうですが、その場合は V8 の初期化が遅くなってしまうそうです。
V8 を組み込んだソフトウェアを配布する時、スナップショットファイルも付属させないといけないのは面倒だな~と思ったのですが、 15 日目の記事 を参考にしてスナップショットファイルを exe に埋め込んでみたところ問題なく動作しました。やったぜ。
V8 を組み込んでみる
V8 を組み込むのに必要なファイルが揃ったので、 Siv3D に組み込んでみましょう。
そして組み込んだものがこちらになります。
>> siv3d-v8 <<
このプロジェクトを使って、色々試してみます。
JavaScript を実行してみる
試しに簡単な JavaScript を実行してみます。
1 * 2 ** 3 | 4;
#include <Siv3D.hpp>
#include "JavaScript.hpp"
void Main()
{
JavaScript js;
// test.js を文字列として読み込む
const auto source = TextReader(L"test.js").readAll();
// test.js をコンパイルする
const auto result = js.compile(source);
// v8 のバージョンを表示
Println(L"V8: ", js.version);
// test.js のソースコードを表示
Println(L"> ", source);
// test.js の実行結果を表示
Println(result);
while (System::Update())
{
}
}
計算してくれました。 ちなみに **
はべき乗演算子です。
JavaScript から Siv3D の関数を呼んでみる
siv3d.println(1 * 2 * 3, 4 - 5, 6 ** 7 / 8 - 9);
siv3d.println(true);
siv3d.println(undefined);
siv3d.println(this);
#include <Siv3D.hpp>
#include "JavaScript.hpp"
void Main()
{
JavaScript js;
js.define()[L"siv3d"] = JS::CreateSiv3D();
const auto source = TextReader(L"test.js").readAll();
const auto result = js.compile(source);
while (System::Update())
{
}
}
JS::CreateSiv3D
という関数で、 Siv3D のいくつかの関数を V8 の形式に変換したものを生成しています。
それを JavaScript でいう global.siv3d
に代入しているので、 JavaScript から siv3d.関数
を呼ぶことができるのです。
この記事を書いている時点では、 JS::CreateSiv3D
で変換される関数は s3d::ClearPrint
と s3d::Println
だけですが、今後の更新で増やしていくつもりです。
(関数を簡単に登録するイイ感じの機能を作りたいと思っています)
JavaScript に値を登録する
// global に追加されている値を全て確認する
for (let value of Object.entries(this)) {
siv3d.println(value[0], value[1]);
// 関数が登録されていたら呼んでみる
if (typeof value[1] === 'function') {
value[1]();
}
}
#include <Siv3D.hpp>
#include "JavaScript.hpp"
void Main()
{
JavaScript js;
js.define()[L"siv3d"] = JS::CreateSiv3D();
js.define()[L"number"] = 123;
js.define()[L"boolean"] = false;
auto object = JS::Object();
object[L"member"] = L"member!";
js.define()[L"object"] = object;
// global.object.member === 'member!'
js.define()[L"function"] = JS::Function([](const JS::Args &args)
{
Println(L"function が呼ばれた!");
});
const auto source = TextReader(L"test.js").readAll();
const auto result = js.compile(source);
while (System::Update())
{
}
}
とりあえず動きましたが、 Object
と Function
の登録が闇なので、今後仕様を変えようと思います。
JavaScript の関数を呼んでみる
siv3d.func = (...args) => {
siv3d.println(...args.reverse());
};
#include <Siv3D.hpp>
#include "JavaScript.hpp"
void Main()
{
JavaScript js;
js.define()[L"siv3d"] = JS::CreateSiv3D();
const auto source = TextReader(L"test.js").readAll();
const auto result = js.compile(source);
js.global()[L"siv3d"][L"func"](1, true, L"text");
while (System::Update())
{
}
}
JavaScript 側で引数を反転しています。 ちなみに、 JavaScript の ...
は Rest Parameters ってやつです。
Promise とか使ってみちゃう?
この記事を書いている時に使用している V8 のバージョンは 5.6.0 なので、 Promise
や await
、 async
といった JavaScript の最新機能も使えちゃいます。
どれくらい最新かというと、 VisualStudio 2015 がシンタックスに対応しておらず、毎回警告を出してくるレベルに最新です(やめて)
await
を使えば、 Siv3D のメインループのような使い方もできちゃいます。
siv3d.main = async () => {
// Siv3D のメインループと同期する
while (await siv3d.update()) {
const count = siv3d.frameCount;
siv3d.setBackgroundHue(count);
siv3d.println(count);
}
};
#include <Siv3D.hpp>
#include "JavaScript.hpp"
// 呼ばれたら Promise を返す Update 関数
v8::Persistent<v8::Promise::Resolver> persistentResolver;
void Update(const JS::Args &args)
{
const auto isolate = args.GetIsolate();
auto resolver = v8::Promise::Resolver::New(isolate);
persistentResolver.Reset(isolate, resolver);
args.GetReturnValue().Set(resolver->GetPromise());
}
void Main()
{
JavaScript js;
auto siv3d = JS::CreateSiv3D();
// Update を登録する
siv3d[L"update"] = JS::Function(Update);
// 色相から背景色を変える関数を登録する
siv3d[L"setBackgroundHue"] = JS::Function([](const JS::Args &args)
{
Graphics::SetBackground(HSV(args[0]->ToNumber()->Value(), 1.0, 1.0));
});
js.define()[L"siv3d"] = siv3d;
const auto source = TextReader(L"test.js").readAll();
const auto result = js.compile(source);
// JavaScript の siv3d.main 関数を呼ぶ
js.global()[L"siv3d"][L"main"]();
while (System::Update())
{
// siv3d.frameCount を更新する
js.global()[L"siv3d"].set(L"frameCount", ToV8::Number(System::FrameCount()));
// Update 関数が呼ばれていたら
if (!persistentResolver.IsEmpty())
{
// JavaScript のメインループを許可する
const auto resolver = v8::Local<v8::Promise::Resolver>::New(ISOLATE, persistentResolver);
resolver->Resolve(ToV8::Boolean(true));
persistentResolver.Reset();
ISOLATE->RunMicrotasks();
}
}
}
Promise
周りのラッパーはまだ実装できていないので生の V8 感がヤバイですが、これを実行するとこんな感じになります。
結局これって何に使うの?
わかりません。もしかしたら Lua みたいな使い方ができるかもしれない?
JavaScript 以外の言語を使うと死んでしまう病 の患者さんは積極的に使ってください。お願いします。
今後の目標としては、このプロジェクトを簡単に Siv3D に組み込める形に持っていきたいと思っています。
まだまだ V8 の勉強中ですので、少しづつ更新していこうかなと。
強い Siv3D ユーザー各位は開発を手伝ってくれてもいいんですよ💗
まとめ
Siv3D はいいぞ!
明日は @ko_n_ke_a さんの記事です。 よろしくお願いします!