はじめに
Emscripten で C/C++ と JavaScript の相互呼び出しにはいくつか方法があります。
- EM_JS, EM_ASM を使って C/C++ のコード内で JavaScript を記述する1
- JavaScript Library (.jslib) を記述する2
- WebIDL を記述して、C/C++ および JavaScript のグルーコードを生成する3
- Embind を使用して、C/C++ のコード内で JavaScript でも使用する関数およびクラスを登録する4
この記事ではそのうちの一つの EM_JS
を使用して、C/C++ と JavaScript の間でプリミティブ型や関数ポインタ、および構造体をやり取りする方法を、例を交えながら紹介します。
対象読者
- Emscripten を使ったことがある
- 初歩的な JavaScript のコードが理解できる
検証環境
- Emscripten 2.0.4
例1: 戻り値だけもらう
# include <stdio.h>
# include <emscripten.h>
EM_JS(int, prompt_number, (), {
var result = prompt('Please input number', '1');
return Number(result || '-1');
});
int main() {
int userChoice = prompt_number();
printf("User Input: %d\n", userChoice);
return 0;
}
EM_JS
マクロの第1引数に関数の戻り値の型を指定できます。
JavaScriptとC/C++のデータのやり取りの型として、指定しても特に問題なく扱えるものは以下の通りです。
- 4バイトまでの整数型
- char
- unsigned char
- short
- unsigned short
- int
- unsigned int
- 浮動小数点型
- float
- double
- 列挙型
- 4バイトまでの整数型にかぎる
- ポインタ型
例2: 引数をつける
EM_JS(int, prompt_number, (int defaultNumber), {
var result = prompt('Please input number', defaultNumber);
return Number(result || '-1');
});
int main() {
int userChoice = prompt_number(300);
printf("User Input: %d\n", userChoice);
return 0;
}
EM_JS
マクロの第3引数に、C/C++での引数の書き方で関数の引数を指定できます。引数名は EM_JS
内のJavaScriptでもそのまま使用可能です。
応用1: 戻り値を文字列にする
# include <stdio.h>
# include <stdlib.h>
# include <emscripten.h>
EM_JS(char*, prompt_name, (), {
var result = prompt('Please input your name', '');
var stringPtr = allocate(intArrayFromString(result || ''), 'i8', ALLOC_NORMAL);
return stringPtr;
});
int main() {
char* userName = prompt_name();
printf("User Name: %s\n", userName);
free(userName);
return 0;
}
allocate
で動的なメモリの確保と文字列での初期化をすることができます。
- emscripten 2.0.4以降でコンパイルオプションに次が必要です。
-s EXPORTED_FUNCTIONS=['_main','_malloc']
- emscripten 2.0.5以降では引数の数が2つになります。
allocate(intArrayFromString(result || ''), ALLOC_NORMAL);
応用2: C/C++から文字列を受け取る
# include <emscripten.h>
EM_JS(void, alertMessage, (const char* message), {
var jsMessage = UTF8ToString(message);
window.alert(jsMessage);
});
int main() {
alertMessage("Hello, Emscripten!");
return 0;
}
文字列の読み込みならば UTF8ToString(ptr, max_length)
, UTF16ToString(ptr)
, UTF32ToString(ptr)
プリミティブ型の読み込みならば getValue(ptr, type)
が便利です。
応用3: C/C++から受け取ったポインタに書き込む
# include <stdio.h>
# include <emscripten.h>
EM_JS(void, prompt_name, (char* ptr, size_t length), {
var result = prompt('Please input your name', '');
stringToUTF8(result, ptr, length);
});
int main() {
char userName[100] = {};
prompt_name(userName, sizeof(userName));
printf("User Name: %s\n", userName);
return 0;
}
文字列書き込みならば stringToUTF8(value, ptr, max_length)
, stringToUTF16(value, ptr, max_length)
, stringToUTF32(value, ptr, max_length)
そのほかプリミティブ型ならば setValue(ptr, value, type)
が便利です。
発展1: 関数ポインタを受け取る
# include <stdio.h>
# include <stdlib.h>
# include <emscripten.h>
typedef void (*CallBack)(char*);
EM_JS(void, prompt_name, (CallBack funcPtr), {
var result = prompt('Please input your name', '');
var stringPtr = allocate(intArrayFromString(result || ''), 'i8', ALLOC_NORMAL);
Module['dynCall']('vi', funcPtr, [stringPtr]);
});
void printMessage(char* message) {
printf("%s\n", message);
free(message);
}
int main() {
prompt_name(&printMessage);
return 0;
}
dynCall
の第一引数には、関数ポインタの型を指定します。printMessage
関数の例では、戻り値 void 型 ("v") + 第1引数 int 型 ("i") で "vi" を指定します。
手元の環境では -s EXTRA_EXPORTED_RUNTIME_METHODS=['dynCall']
の指定が必要でした。
発展2: JavaScript から構造体を返す
# include <stdio.h>
# include <emscripten.h>
struct Vec2 {
float x, y;
};
# define EM_JS_EXT(ret, name, params, paramsInJS, ...) \
_EM_JS_CPP_BEGIN \
extern ret name params EM_IMPORT(name); \
__attribute__((used, visibility("default"))) \
const char* __em_js__##name() { \
return #paramsInJS "<::>" #__VA_ARGS__; \
} \
_EM_JS_CPP_END
EM_JS_EXT(Vec2, makeVec2, (), (retPtr), {
setValue(retPtr + 0, 300.0, 'float'); // 0 = Vec2::x
setValue(retPtr + 4, 400.0, 'float'); // 4 = Vec2::y
});
int main() {
Vec2 v = makeVec2();
printf("%f, %f\n", v.x, v.y);
}
構造体を返す関数は、Emscripten の WebAssembly 実装において、第一引数に戻り値となる構造体へのポインタ (呼び出し元がこのメモリ領域をスタックに用意する) が渡される関数に変換されることを利用しています。5
また、この仕様によって、C/C++ 側と JavaScript 側の関数の引数が異なってくるため、EM_JS
の実装を参考に、C/C++ 側と JavaScript 側の引数を指定できる EM_JS_EXT
を定義しています。
発展3: JavaScript から std::string を返す
# include <stdio.h>
# include <string>
# include <emscripten.h>
# define EM_JS_EXT(ret, name, params, paramsInJS, ...) \
extern ret name params EM_IMPORT(name); \
_EM_JS_CPP_BEGIN \
__attribute__((used, visibility("default"))) \
const char* __em_js__##name() { \
return #paramsInJS "<::>" #__VA_ARGS__; \
} \
_EM_JS_CPP_END
EM_JS_EXT(std::string, prompt_name, (), (retPtr), {
var stackBase = stackSave();
var result = prompt('Please input your name', '');
var stringPtr = allocate(intArrayFromString(result || ''), 'i8', ALLOC_STACK);
Module["_init_string"](retPtr, stringPtr);
stackRestore(stackBase);
});
__attribute__((used, export_name("init_string")))
std::string init_string(const char* str) {
return std::string(str);
}
int main() {
std::string userName = prompt_name();
printf("User Name: %s\n", userName.c_str());
return 0;
}
Emscripten の WebAssembly 実装における構造体を返す関数の変換は、JavaScript 側から C/C++ 側の関数を呼び出す際にも注意を払う必要があります。
__attribute__((used))
属性で修飾した関数は、出力される .wasm ファイルに残され、JavaScript 側から Module['_(関数名)']
で利用できるようになります。
さらに、__attribute__((used, export_name("(関数名)")))
属性で関数を修飾すると、その名前でJavaScript 側から利用できるようになります。
stackSave
stackRestore
関数は、それぞれスタックの保存、スタックの復元を行う関数です。この関数で囲んだ区間では、stackAlloc
allocate(.., .., ALLOC_STACK)
といったスタック領域からのメモリ確保を行う関数を使うことができます。
-
Calling JavaScript from C/C++ を参照のこと ↩
-
Implement a C API in JavaScript を参照のこと ↩
-
WebIDL Binder を参照のこと ↩
-
WebAssembly BasicCABI を参照のこと ↩