Siv3D に JavaScript を組み込む

  • 13
    Like
  • 0
    Comment

これは Siv3D Advent Calendar 2016 - 20 日目の記事です。


「 Siv3D はいいぞ!」というセリフをここ数ヶ月で何回もききました。
これは使ってみるしかない! ということで、開発環境を整えていたその時!

JavaScript 以外の言語を使うと死んでしまう病 に感染してしまったのです。

Siv3D を使ってみたい... でも JavaScript 以外を使うと死んでしまう...
ん? それなら Siv3D で JavaScript を使えばいい のでは!!!?!??!?

 JavaScript ってなんぞや?

アレです。 Java ではないです。
この記事をウェブブラウザで読んでいただいているなら、ここでも JavaScript は働いています。
この記事をウェブブラウザ以外で読んでいる特殊な方は見なかったことにしてください。

JavaScript は名前に入っている通りのスクリプト言語なので、それを実行するインタプリタ(いわゆる JavaScript エンジン)が存在します。
色々な種類のウェブブラウザがありますが( Internet Explorer ? なにそれおいしいの? )JavaScript エンジンもいくつかあるみたいです。
もしかしたら、 Siv3D に組み込める JavaScript エンジン があるかもしれません。

 選ばれたのは、 V8 でした。

V8_JavaScript_engine_logo_2.svg.png

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 2015V8 - 5.6.0 (candidate) をビルドした時に参考にしたサイト様を載せておきます。

上記のサイトを参考に作業を進めると all.sln が生成されるので、あとはそれを VisualStudio で開いてビルドするだけです。
ビルドに成功すると、 V8 を使う際に必要な DLL や、スナップショットという仕組みに必要な追加ファイルが生成されます。

ちなみに、ビルド時のオプションでスナップショットの追加ファイルを使わない設定にできるそうですが、その場合は V8 の初期化が遅くなってしまうそうです。
V8 を組み込んだソフトウェアを配布する時、スナップショットファイルも付属させないといけないのは面倒だな~と思ったのですが、 15 日目の記事 を参考にしてスナップショットファイルを exe に埋め込んでみたところ問題なく動作しました。やったぜ。

 V8 を組み込んでみる

V8 を組み込むのに必要なファイルが揃ったので、 Siv3D に組み込んでみましょう。

そして組み込んだものがこちらになります。
>> siv3d-v8 <<

このプロジェクトを使って、色々試してみます。

 JavaScript を実行してみる

試しに簡単な JavaScript を実行してみます。

test.js
1 * 2 ** 3 | 4;
Main.cpp

#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())
    {
    }
}


ddddddd.PNG

計算してくれました。 ちなみに ** はべき乗演算子です。

 JavaScript から Siv3D の関数を呼んでみる

test.js
siv3d.println(1 * 2 * 3, 4 - 5, 6 ** 7 / 8 - 9);
siv3d.println(true);
siv3d.println(undefined);
siv3d.println(this);

Main.cpp
#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())
    {
    }
}

wwre.PNG

JS::CreateSiv3D という関数で、 Siv3D のいくつかの関数を V8 の形式に変換したものを生成しています。
それを JavaScript でいう global.siv3d に代入しているので、 JavaScript から siv3d.関数 を呼ぶことができるのです。

この記事を書いている時点では、 JS::CreateSiv3D で変換される関数は s3d::ClearPrints3d::Println だけですが、今後の更新で増やしていくつもりです。
(関数を簡単に登録するイイ感じの機能を作りたいと思っています)

 JavaScript に値を登録する

test.js

// global に追加されている値を全て確認する
for (let value of Object.entries(this)) {

    siv3d.println(value[0], value[1]);

    // 関数が登録されていたら呼んでみる
    if (typeof value[1] === 'function') {
        value[1]();
    }

}
Main.cpp

#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())
    {
    }

}


img.PNG

とりあえず動きましたが、 ObjectFunction の登録が闇なので、今後仕様を変えようと思います。

 JavaScript の関数を呼んでみる

test.js
siv3d.func = (...args) => {
    siv3d.println(...args.reverse());
};
Main.cpp

#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())
    {
    }

}

gafs.PNG

JavaScript 側で引数を反転しています。 ちなみに、 JavaScript の ... は Rest Parameters ってやつです。

 Promise とか使ってみちゃう?

この記事を書いている時に使用している V8 のバージョンは 5.6.0 なので、 Promiseawaitasync といった JavaScript の最新機能も使えちゃいます。
どれくらい最新かというと、 VisualStudio 2015 がシンタックスに対応しておらず、毎回警告を出してくるレベルに最新です(やめて)
await を使えば、 Siv3D のメインループのような使い方もできちゃいます。

test.js
siv3d.main = async () => {

    // Siv3D のメインループと同期する
    while (await siv3d.update()) {

        const count = siv3d.frameCount;

        siv3d.setBackgroundHue(count);
        siv3d.println(count);

    }

};

Main.cpp

#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 感がヤバイですが、これを実行するとこんな感じになります。

123213213.gif

 結局これって何に使うの?

わかりません。もしかしたら Lua みたいな使い方ができるかもしれない?
JavaScript 以外の言語を使うと死んでしまう病 の患者さんは積極的に使ってください。お願いします。

今後の目標としては、このプロジェクトを簡単に Siv3D に組み込める形に持っていきたいと思っています。
まだまだ V8 の勉強中ですので、少しづつ更新していこうかなと。

強い Siv3D ユーザー各位は開発を手伝ってくれてもいいんですよ💗

 まとめ

Siv3D はいいぞ!



明日は @ko_n_ke_a さんの記事です。 よろしくお願いします!