Rust(wasm)とJavaScript(WebGL)のデータ受け渡し Rust+WebGLでポリゴン描画 (1/2)

前回の記事は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)

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

認証:数字を入力してください(必須) * Time limit is exhausted. Please reload CAPTCHA.