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

0x90

一回休み

EmscriptenでCからJavascriptの関数を呼び出す

Javascriptは嫌だ!(定期)

ということでRustでJavascriptの代替をしちゃおうと思ったのですが、DOM操作がかなり微妙でした。ということでRustに行く前にCでJavascriptの関数を呼び出すところからのメモ。

RustからのDOM操作の現状

今のところあるライブラリは

github.com

のもの。ちょっと関数名もアドホックだし実装されてないのもあるな~とかそういう問題もあるんですが、まあじゃあどうやってJS側を呼んでいるか見てみましょう。

github.com

はい。ということで、

extern "C" {
    pub fn emscripten_asm_con(s: *const libc::c_char);
    pub fn emscripten_asm_const(s: *const libc::c_char);
    pub fn emscripten_asm_const_int(s: *const libc::c_char, ...) -> libc::c_int;
    pub fn emscripten_pause_main_loop();
    pub fn emscripten_set_main_loop(m: extern fn(), fps: libc::c_int, infinite: libc::c_int);
}

系をひたすら呼んでいます。オブジェクトはwindow.WEBPLATFORMというグローバルな配列に突っ込んで、そのindexで管理しているようです。配列から消してないしこれじゃGC働かないじゃん!とかそういうこともあるんですが(まあこれはfastbins的な実装をすればいいでしょうが…)、それ以前にこの実装だと来るべきwasm時代にどうなるかわからんとかそういうのもあってあくまでもアドホックな実装の域を出ていないように思います。

じゃあどうするの

Emscriptenには、val.hというJavascriptの関数/オブジェクトをC++から呼ぶライブラリがあります。

val.h (under-construction) — Emscripten 1.36.14 documentation (ドキュメントは腐っています)

まあよくわかりませんが、多分wasmのバックエンドもサポートする気はあるのだと思います。ということで、このひとを使ってみたい。

これのRustバインディングがほしいのですが、残念ながらこのライブラリはC++でしか動きません。そこでまずはCでのwrapperを作ってみたという話です。

(余談ですがDはマングリングをうまくやってくれるらしいという噂を聞きました。ほんとでしょうか。)

val.hは何してるの

そこで、まずはval.hの中身を見ていきましょう。val.hはそもそもコードサンプルが少なくてその時点で心が折れそうになるのですが、なんとか

Embind — Emscripten 1.36.14 documentation

を見つけてきました。これを見ると、valというクラスを通してJavascriptを操作しているようです。

そしたら、val.hの中身を見てみましょう。

github.com

…可変長テンプレートや実行時型情報などC++の闇をこれでもかと詰め込んだ素敵なコードですね。最初はvalクラスをwrapするCのコードでも書こうかと思ったのですが、闇が深すぎて断念しました。

辛い~と思いながらコードを見ていくのですが、実はval.hで呼んでいるのは組み込みのC関数であることに気が付きます。

        // Implemented in JavaScript.  Don't call these directly.
        extern "C" {
            void _emval_register_symbol(const char*);

            enum {
                _EMVAL_UNDEFINED = 1,
                _EMVAL_NULL = 2,
                _EMVAL_TRUE = 3,
                _EMVAL_FALSE = 4
            };

            typedef struct _EM_VAL* EM_VAL;
            typedef struct _EM_DESTRUCTORS* EM_DESTRUCTORS;
            typedef struct _EM_METHOD_CALLER* EM_METHOD_CALLER;
            typedef double EM_GENERIC_WIRE_TYPE;
            typedef const void* EM_VAR_ARGS;

            void _emval_incref(EM_VAL value);
            void _emval_decref(EM_VAL value);

            void _emval_run_destructors(EM_DESTRUCTORS handle);

            EM_VAL _emval_new_array();
            EM_VAL _emval_new_object();
            EM_VAL _emval_new_cstring(const char*);

            EM_VAL _emval_take_value(TYPEID type, EM_VAR_ARGS argv);

            EM_VAL _emval_new(
                EM_VAL value,
                unsigned argCount,
                const TYPEID argTypes[],
                EM_VAR_ARGS argv);

            EM_VAL _emval_get_global(const char* name);
            EM_VAL _emval_get_module_property(const char* name);
            EM_VAL _emval_get_property(EM_VAL object, EM_VAL key);
            void _emval_set_property(EM_VAL object, EM_VAL key, EM_VAL value);
            EM_GENERIC_WIRE_TYPE _emval_as(EM_VAL value, TYPEID returnType, EM_DESTRUCTORS* destructors);

            bool _emval_equals(EM_VAL first, EM_VAL second);
            bool _emval_strictly_equals(EM_VAL first, EM_VAL second);

            EM_VAL _emval_call(
                EM_VAL value,
                unsigned argCount,
                const TYPEID argTypes[],
                EM_VAR_ARGS argv);

            // DO NOT call this more than once per signature. It will
            // leak generated function objects!
            EM_METHOD_CALLER _emval_get_method_caller(
                unsigned argCount, // including return value
                const TYPEID argTypes[]);
            EM_GENERIC_WIRE_TYPE _emval_call_method(
                EM_METHOD_CALLER caller,
                EM_VAL handle,
                const char* methodName,
                EM_DESTRUCTORS* destructors,
                EM_VAR_ARGS argv);
            void _emval_call_void_method(
                EM_METHOD_CALLER caller,
                EM_VAL handle,
                const char* methodName,
                EM_VAR_ARGS argv);
            EM_VAL _emval_typeof(EM_VAL value);
        }

Don’t call these directly.とか言っているので、仕様が変わる臭いがプンプンするんですが、まあそうも言っていられません。こいつを直に叩かせていただきましょう。

これでXSSJavascript界のHello, world!ことalert(1)を呼んでみましょう。

Globalオブジェクトを取得する

これだけです。

EM_VAL alert = _emval_get_global("alert");

簡単!光明が見えてきた気がしますね。そんなあなたは騙されています。

メソッドを呼ぶ

_emval_callでalertを呼びます。_emval_callのプロトタイプ宣言を見てみましょう。

EM_VAL _emval_call(EM_VAL value, unsigned argCount, const TYPEID argTypes[], EM_VAR_ARGS argv);

うげえ。valueは呼ばれるオブジェクトっぽいですね。EM_VAR_ARGSはvoid*なので、おそらく引数のリストでしょうが、実際に何が突っ込まれているかはわかりません。argCountがその長さで、argTypesが型情報だろうという推論もできます。

ここでTYPEIDってなんじゃいと思ってみると、実はval.hで定義されていません。仕方がないのでレポジトリに検索をかけると、

github.com

でvoid *になっていることがわかります。ということで、問題はTYPEIDを取得する方法とEM_VAR_ARGSの中身となります。実際_emval_callを使っているのはinternalCall関数で、

        template<typename Implementation, typename... Args>
        val internalCall(Implementation impl, Args&&... args)const {
            using namespace internal;

            WithPolicies<>::ArgTypeList<Args...> argList;
            WireTypePack<Args...> argv(std::forward<Args>(args)...);
            return val(
                impl(
                    handle,
                    argList.getCount(),
                    argList.getTypes(),
                    argv));
        }

こんななりをしています。ということで、WithPolicies::ArgTypeListとWireTypePackの中身を見ればいいわけです。

WireTypePack

簡単な方から。WireTypePackはだいたいこんな感じです。

        template<typename... Args>
        struct WireTypePack {
            WireTypePack(Args&&... args) {
                GenericWireType* cursor = elements.data();
                writeGenericWireTypes(cursor, std::forward<Args>(args)...);
            }

            operator EM_VAR_ARGS() const {
                return elements.data();
            }

        private:
            std::array<GenericWireType, PackSize<Args...>::value> elements;
        };

ということで、単に引数のリストをぶち込むだけでよいようです。

WithPolicies::ArgTypeList

こちらはwire.h中で、

            template<typename... Args>
            struct ArgTypeList {
                unsigned getCount() const {
                    return sizeof...(Args);
                }

                const TYPEID* getTypes() const {
                    return ArgArrayGetter<
                        typename MapWithIndex<TypeList, MapWithPolicies, Args...>::type
                    >::get();
                }
            };

こんなかんじになっています。ArgArrayGetterは

        template<typename... Args>
        struct ArgArrayGetter<TypeList<Args...>> {
            static const TYPEID* get() {
                static constexpr TYPEID types[] = { TypeID<Args>::get()... };
                return types;
            }
        };

なので、TypeIDを見てみると

        template<typename T>
        struct TypeID {
            static constexpr TYPEID get() {
                return LightTypeID<T>::get();
            }
        };

で、

        template<typename T>
        struct LightTypeID {
            static constexpr TYPEID get() {
                typedef typename Canonicalized<T>::type C;
                return (has_unbound_type_names || std::is_polymorphic<C>::value)
                    ? &typeid(C)
                    : CanonicalizedID<C>::get();
            }
        };

となっています。

    #ifndef EMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES
    #define EMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES 1
    #endif


    #if EMSCRIPTEN_HAS_UNBOUND_TYPE_NAMES
    constexpr bool has_unbound_type_names = true;
    #else
    constexpr bool has_unbound_type_names = false;
    #endif

とありますから、まあ大体実行時型情報typeid©のリストだと思ってよさそうです。

ということで

こんなファイルを作り、

#ifndef TYPEID_H
#define TYPEID_H

#ifdef __cplusplus
extern "C" {
#endif
  typedef const void* TYPEID;

  extern TYPEID int_id;
#ifdef __cplusplus
}
#endif

#endif
#include <typeinfo>

#include "typeid.h"


TYPEID int_id = &typeid(int);

あとはこれでおk

#include "val_c.h"
#include "typeid.h"
#include <stdio.h>

int main() {
  EM_VAL alert = _emval_get_global("alert");
  int arg = 1;
  TYPEID args[1] = {int_id};

  _emval_call(alert, 1, args, &arg);

  return 0;
}

コンパイルはこんな感じ。

$ em++ -o typeid.bc typeid.cpp
$ emcc -o test.o test.c
$ emcc -o test.html --bind test.o typeid.bc

もっと複雑な引数(stringとか)はまた明日。