前回の記事はJavaScriptでのWebGLの画面クリアだけだったので、今度はRustによるポリゴン描画を行います。WebGLのAPIを呼び出すだけで簡単できると考えていましたが、データの受け渡しが思った以上に複雑でした。OpenGLの知識があればポリゴン描画自体は難しくないので、主にRust⇔JavaScript間のデータの受け渡し方法について書いていきます。
WebGLについての誤算
WebGLもOpenGLと同じようにバッファやシェーダーなどのリソースを作成した際は、GLuintの整数値(ハンドル)が返ってくると思っていました。違いました。WebGLBufferやWebGLShaderといったオブジェクトが返され、これを使ってGL命令を実行。Rust(wasm)で直接JavaScript側のオブジェクトを管理できない様なので対策が必要です。それと、Rust(wasm)から直接WebGL(WebAPI)は呼び出せないため、JavaScriptを経由する必要があります。
Rust⇔JavaScript間のデータ受け渡し
Rustの構造体やJavaScriptのオブジェクトなどを直接やり取りすることはできないため、データ受け渡しする処理を用意しないといけません。とりあえず今回必要なものは、RustからJSオブジェクトの間接的な保持、文字列の受け渡し(シェーダー)、Rust⇒JSのバイナリデータ(頂点情報)転送です。
直接受け渡しできるデータ
JavaScriptで扱う64bit浮動小数で表現できる数値型のみです。32bit以下の整数(u32 i32 u16 u8など)、32bit浮動小数(f32)、64bit浮動小数(f64)です。あとwasmがターゲットの場合、usizeは32bit整数になるようなので使用可能です。
RustによるJSオブジェクトの管理
整数値による間接的なオブジェクト管理、いわゆるハンドルによる管理を採用します。Win32(C/C++)とかでよく使われている方法です。JS側に配列を用意し、作成したWebGLオブジェクトを格納、その配列index(ハンドル)をRust側に返す。Rust側はそのハンドルでWebGLのAPIを呼び出す。というような仕様にします。
これでいいのか不安だったので、wasm-bindgen/web-sysのWebGL使用例を調べてみたところ、同じ方法で実装していました。この方法で問題なさそうです。
文字列 Rust⇔JavaScript
Rust(wasm)のメモリは、JavaScriptからはArrayBufferとしてアクセスできます。Rustのポインタ=ArrayBuffer先頭からのオフセット値なっているので、Rust文字列のポインタ(オフセット値:usize)とサイズを受け取れば、Rust側のメモリ(文字列)にアクセスすることができます。ただし、RustはUTF8、JavaScriptはUTF16なので文字コードの変換が必要になります。
UTF8⇔UTF16 文字コード変換(JavaScript)
TexDecoder、TexEncoderという便利なものが。Edgeが対応してませんでした。一部分の機能しか利用しないのでpolyfillはせずに文字コード変換をwikipediaを見ながら自作しました(簡単な動作確認のみ)。Uint8Array⇒StringとString⇒Uint8Array。高速な変換が必要になればRustの文字コード変換を使う予定。
.js String(UTF16) ⇒ Uint8Array(UTF8)function string_to_utf8(str) {
var u8_ary = new Uint8Array(str.length*4);//最長*4
var ptr8 = 0;
for(var i = 0;i < str.length; ++i){
// utf16 > utf32
var utf32 = 0;
var utf16_0 = str.charCodeAt(i);
if( utf16_0 >= 0xd800 && utf16_0 < 0xdc00){//high
if(i+1>str.length)return;// 不正な文字列
var utf16_1 = str.charCodeAt(++i);
if( utf16_1 >= 0xdc00 && utf16_1 < 0xe000){//low
utf32 = ((utf16_0 & 0x03c0)<<10) + 0x10000;
utf32 += (utf16_0 & 0x003f)<<10;
utf32 += (utf16_1 & 0x03ff);
//000u uuuu xxxx xxxx xxxx xxxx
//1101 10ww wwxx xxxx 1101 11xx xxxx xxxx ( wwww = uuuuu-1
}else{
return;// 不正な文字列
}
}else if(utf16_0 >= 0xdc00 && utf16_0 < 0xe000){
return; // 不正な文字列
}else{
utf32 = utf16_0;
}
if(ptr8+4 > u8_ary.length)return;//不正な文字列
// utf32 > utf8
if(utf32 < 0x80){
u8_ary[ptr8++] = utf32;
}else if(utf32 < 0x800){
u8_ary[ptr8++] = 0xc0 + (utf32 >> 6);
u8_ary[ptr8++] = 0x80 + (utf32 & 0x3f);
}else if(utf32 < 0x10000){
u8_ary[ptr8++] = 0xe0 + (utf32 >> 12);
u8_ary[ptr8++] = 0x80 + ((utf32 >> 6) & 0x3f);
u8_ary[ptr8++] = 0x80 + (utf32 & 0x3f);
}else if(utf32 < 0x110000){
u8_ary[ptr8++] = 0xf0 + (utf32 >> 18);
u8_ary[ptr8++] = 0x80 + ((utf32 >> 12) & 0x3f);
u8_ary[ptr8++] = 0x80 + ((utf32 >> 6) & 0x3f);
u8_ary[ptr8++] = 0x80 + (utf32 & 0x3f);
}
}
return Uint8Array.from(u8_ary.subarray(0,ptr8));
}
.js Uint8Array(UTF8) ⇒ String(UTF16)function utf8_to_string(u8_ary)
{
var str = "";
var ptr8 = 0;
var len8 = u8_ary.length;
// Stringの1文字追加効率悪い? とりあえずキャッシュ
var str_cache = new Uint16Array(256);
var cptr = 0;
do {
var utf32 = 0;
do {//utf8 > utf32
// 0xxx xxxx
var c0 = u8_ary[ptr8++];
if( c0 < 0x80 ){utf32 = c0; break;}
// 110yyyyx
if( (c0 & 0xe0) == 0xc0 ){ //len = 2
if( ptr8+1 > len8)break;
if( (c0 & 0x1e) == 0 )break;// yyyyどれか必ず1
var c1 = u8_ary[ptr8++];
if( (c1 & 0xc0) != 0x80 )break;//10ではない
utf32 = ((c0 & 0x1f)<<6) | (c1 & 0x3f);
break;
}
// 1110yyyy 10yxxxxx 10xxxxxxx
// yyyy yxxx xxxx xxxx
if( (c0 & 0xf0) == 0xe0 ){ //len = 3
if( ptr8+2 > len8)break;
var v = (c0 & 0x0f) << 12;
var c1 = u8_ary[ptr8++];
if( (c1 & 0xc0) != 0x80 )break;//10ではない
v |= (c1 & 0x3f) << 6;
var c2 = u8_ary[ptr8++];
if( (c2 & 0xc0) != 0x80 )break;//10ではない
v |= (c2 & 0x3f);
if( (v & 0xf800) == 0 )break;//yyyyyどれか必ず1
utf32 = v;
break;
}
// 1111 0yyy 10yy xxxx 10xxx xxx 10xx xxxx
// y yyyy xxxx xxxx xxxx xxxx
if ( (c0 & 0xf8) == 0xf0 ) { //len = 4
if( ptr8+3 > len8)break;
var v = (c0 & 0x07) << 18;
var c1 = u8_ary[ptr8++];
if( (c1 & 0xc0) != 0x80 )break;//10ではない
v |= (c1 & 0x3f) << 12;
var c2 = u8_ary[ptr8++];
if( (c2 & 0xc0) != 0x80 )break;//10ではない
v |= (c2 & 0x3f) << 6;
var c3 = u8_ary[ptr8++];
if( (c3 & 0xc0) != 0x80 )break;//10ではない
v |= (c3 & 0x3f);
if( (v & 0x1f0000) == 0 )break;//yyyyyどれか必ず1
utf32 = v;
break;
}
}while(0);
// 不正なコード
if(utf32 <= 0 || utf32 > 0x10ffff)return;
// キャッシュ書き込み
if(cptr+2 > str_cache.length){
str += String.fromCharCode.apply(null,str_cache.slice(0,cptr));
cptr = 0;
}
if(utf32 < 0x10000) {
str_cache[cptr++] = utf32;
}else{
//000u uuuu xxxx xxxx xxxx xxxx
//1101 10ww wwxx xxxx 1101 11xx xxxx xxxx ( wwww = uuuuu-1
var u1 = (utf32 & 0x01f0000) - 0x10000;
var u2 = (utf32 & 0x000fc00);
var u3 = (utf32 & 0x00003ff);
str_cache[cptr++] = 0xd800 + (u1>>10) + (u2>>10);
str_cache[cptr++] = 0xdc00 + u3;
}
}while(ptr8 < len8);
str += String.fromCharCode.apply(null,str_cache.slice(0,cptr));
// apply 配列の各要素を引数に分解 スタックに積まれるので数に制限あり
return str;
}
.rs Rustで文字コード変換// Vec<u16>(UTF16) ⇒ String(UTF8)
fn utf16_to_string(b : &Vec<u16>) -> String {
std::char::decode_utf16(b.iter().cloned())
.map(|r| r.unwrap_or(std::char::REPLACEMENT_CHARACTER))
.collect::<String>()
}
// String(UTF8) ⇒ Vec<u16>(UTF16)
fn string_to_utf16(s : &String) -> Vec<u16> {
s.encode_utf16().collect::<Vec<u16>>()
}
Rust⇒JS 引数に文字列
文字列のアドレスとサイズをJavaScriptに渡します。UTF8からUTF16への変換が必要。
例.rs RustからJSのcolosle.logextern "C" {
fn js_console_log(ptr : usize, size : usize);
}
#[no_mangle]
fn hello_rust() {
let s : &str = "こんにちは、JavaScript";//UTF8
unsafe {
js_console_log(s.as_ptr() as usize, s.len());
}
}
例.js RustからJSのcolosle.logvar wasm_inst; // = WebAssembly.instantiate()
function js_console_log(str_ptr, str_byte_len) {
// wasmメモリ ArrayBuffer
var u8_array = new Uint8Array(wasm_inst.exports.memory.buffer);
// 文字列切り出し
var u8_sub = u8_array.subarray(str_ptr, str_ptr + str_byte_len);
console.log( utf8_to_string(u8_sub) );
}
Rust⇔JS 文字列受け渡し
JSからString(Rust)のメソッドを呼び出す関数を用意します。Stringのnew、reserve、as_mut_ptr、set_lenの4つ。RustとJS間でStringのポインタ(*mut String as usize)を使って文字列の受け渡しができます。最初、mallocのような関数を使う予定でしたがポインタとサイズの2つを受け渡す必要があるので、Stringのポインタ1つで済むこの方法を採用しました。使い方を間違うとRust側のメモリをぶっ壊すので取扱注意。WebGLのシェーダーコンパイルエラーログ取得で使用します。
例.rs JSから文字列取得extern "C" {
fn js_get_string_from_js() -> usize;
}
// JavaScriptから文字列取得
fn get_string_from_js() -> String {
unsafe {
let ptr = js_get_string_from_js();
if( ptr == 0){ String::new() }
// 生ポインタから所有権を取り戻す
else { *Box::from_raw(ptr as *mut String) }
}
}
// JavaScript用関数
// Stringをヒープに配置&生ポインタ(所有権放棄)
#[no_mangle]
fn rust_js_string_new() -> usize {
let stack = Box::new( String::new() );
unsafe { Box::into_raw(stack) as usize }
}
// 文字列領域を確保
#[no_mangle]
fn rust_js_string_reserve(str_ptr : usize, add_size : usize) -> usize {
if str_ptr == 0 { return 0; }//nullpo
unsafe {
let s = str_ptr as *mut String;
(*s).reserve(add_size);// try_reserveは1.37stableでは使えない
(*s).as_mut_ptr() as usize
}
}
// 文字列領域を確定
#[no_mangle]
fn rust_js_string_set_len(str_ptr : usize, len : usize) {
if str_ptr == 0 { return; }//nullpo
unsafe {
let s = str_ptr as *mut String;
(*s).as_mut_vec().set_len(len);
}
}
例.js Rustへ文字列を渡すvar wasm_inst; // = WebAssembly.instantiate()
// return : *mut String
function get_string_from_js() {
var js_str = "Rust:JSから文字列を受け取る";
var utf8 = string_to_utf8(js_str);
// String::new()
var str_ptr = wasm_inst.exports.rust_js_string_new();
// String::reserve(),as_mut_ptr()
var buff_ptr = wasm_inst.exports.rust_js_string_reserve(str_ptr, utf8.length);
if( buff_ptr == 0 )return 0;//メモリ確保失敗
// 書き込み
var u8_array = new Uint8Array(wasm_inst.exports.memory.buffer);
var u8_buff = u8_array.subarray(buff_ptr, buff_ptr+utf8.length);
u8_buff.set(utf8);
// String::as_mut_vec().set_len()
wasm_inst.exports.rust_js_string_set_len(str_ptr, utf8.length);
return str_ptr;
}
バイナリデータ(頂点情報、テクスチャ画像など)
文字列と同じ方法で受け渡しします。数値データであれば、文字コードの変換などが不要なため、単純なメモリアクセスだけになります。
例.rs JSへバイナリーデータを渡すextern "C" {
fn set_binary_to_js(ptr: usize, byte_size: usize);
}
#[no_mangle]
fn hello_rust() {
let vertex : [f32;9] = [
0.0, 0.0, 0.0,
1.0, 0.0, 0.0,
0.0, 1.0, 0.0 ];
unsafe{
set_binary_to_js(vertex.as_ptr() as usize, vertex.len()*4);
}
}
例.js Rustからバイナリーデータを受け取るvar wasm_inst; // = WebAssembly.instantiate()
function set_binary_to_js(ptr, byte_size)
{
var u8_array = new Uint8Array(wasm_inst.exports.memory.buffer);
var buff_u8 = u8_array.subarray(ptr, ptr+byte_size);
//console.log(buff_u8);
// WebGL 頂点データやテクスチャはバイナリデータ
// 頂点データがf32でもUint8ArrayでOk
//gl.bufferData(buff_u8, gl.STATIC_DRAW);
//gl.texImage2D(,,,,,,,,buff_u8);
//などwasmのメモリをそのまま渡せる
//(メモリアライメントは必要かも)
}
まとめ
RustからWebGLを呼び出すのに必要な準備が整ったので、次回実際にポリゴン描画を行うプログラムを作成します。
JavaScriptからArrayBufferを通してRust側のメモリにアクセスできるので、大抵のデータ送受信、オブジェクト操作は実装可能です。面倒くさいですが。ただし、使い過ぎはRustのメモリ管理の厳密さを消してしまうので注意が必要です。個人的な設計思想ですが、RustのソースコードにJavaScript特有の処理が現れないようにして、JavaScrit(WebGL)をC/C++(OpenGL)に置き換えられるぐらい(実際にはしませんが)の依存関係と独立性を持たせるつもりです。ブラウザ内にwasmで動く仮想ゲーム機とそのAPIを作る感じです(WASIを少し意識してます)。
代表的なバインダーwasm-bindgenのWebGLチュートリアルではRustソースコードにFloat32Arrayなどの記述があったため、利用を見送ります。
>> Rust(wasm)とJavaScript(WebGL)のデータ受け渡し Rust+WebGLでポリゴン描画 (2/2)