Godot 4 & WebAssemblyで様々なデータフォーマットを処理
2025.07.06
どもです。
前回「Godot 4 & WebAssemblyで、Hello WebAssembly! – godot-wasm」の記事を書きましたが、もうちょっと踏み込んだ内容になります。
godot-wasmを用いたサンプルは、こちらのページに色々なデモがありますので、参考にすると良いかと思います。
WebAssembly化されたDoomも、ヌルヌル動いて楽しいです。
Godot側のGDScriptは、わずか34行。
extends Control @onready var wasm = Wasm.new() @onready var memory = WasmMemory.new() var image = Image.new() var keys = { KEY_ENTER: 13, KEY_BACKSPACE: 127, KEY_SPACE: 32, KEY_LEFT: 0xac, KEY_RIGHT: 0xae, KEY_UP: 0xad, KEY_DOWN: 0xaf, KEY_CTRL: 0x80+0x1d, KEY_ALT: 0x80+0x38, KEY_ESCAPE: 27, KEY_TAB: 9, KEY_SHIFT: 16 } func _ready(): for k in range(KEY_A, KEY_Z + 1).map(func(x): return { x: x + 32 }) + range(KEY_0, KEY_9 + 1).map(func(x): return { x: x }) + range(KEY_F1, KEY_F12 + 1).map(func(x): return { x: 187 + x - KEY_F1 }): keys.merge(k) image = Image.create(640, 400, false, Image.FORMAT_RGBA8) $TextureRect.texture.set_image(image) memory.grow(108) var imports = { "functions": { "js.js_console_log": [self, "stdout"], "js.js_draw_screen": [self, "draw_screen"], "js.js_milliseconds_since_start": [Time, "get_ticks_msec"], "js.js_stdout": [self, "stdout"], "js.js_stderr": [self, "stderr"] }, "memory": memory, } wasm.load(FileAccess.get_file_as_bytes("res://doom.wasm"), imports) wasm.function("main", [0, 0]) func _process(_delta): wasm.function("doom_loop_step") func _input(event): if event is InputEventKey and !event.is_echo(): wasm.function("add_browser_event", [int(!event.is_pressed()), keys.get(event.keycode, 0)]) func draw_screen(offset): image.set_data(640, 400, false, Image.FORMAT_RGBA8, memory.seek(offset).get_data(640 * 400 * 4)[1]) $TextureRect.texture.update(image) func stdout(offset, length): print(memory.seek(offset).get_utf8_string(length)) func stderr(offset, length): push_warning(memory.seek(offset).get_utf8_string(length))
WebAssemblyの流用性がわかりますね。
WebAssemblyの仕様に関してはいずれまとめたいなと思いつつ、今回は「wasm32-unknown-unknown」フォーマットで出力されたWebAssemblyをGodotにimportして、様々なデータフォーマットのデータのやり取りしよう。といったところになります。
以前は、Web用として出力するために、wasm-bindgenを用いていたのですが、wasm32-unknown-unknownだとまあまあ手間がかかりますね。せめて、WebAssembly Component Model で扱えれば良いなと思いました。
様々なデータフォーマットをWASMに送信・取得
では早速。
先にソースなどを参照したい方はこちらとなります。
前回同様「godot-wasm」を利用します。
左側のInputにて、String、Int、ByteArray、JSONの任意の値を入力し「Submit」を押下すると結果が出力されます。
出力に関して、
- DebugLogは、入力値をそのまま加工なしで表示します。
- WasmLogは、入力をWasmに渡し、変換なしで取得し表示します。
- WasmCalcLogは、入力をWasmに渡し、Wasmによる処理後に取得し表示します。(String:反転、Int:2倍、ByteArray:反転、JSON:nameは反転、levelは2倍)
Godot と WebAssembly (Rust) の間で String、int、PackedByteArray、JSON の値を保存、取得する流れですが、すべてのデータは store_* 関数によってリニアメモリに格納され、WebAssembly から返されるメモリポインタを使用して取得する形となります。
func store_string(text: String, offset := 0) -> int: var bytes: PackedByteArray = text.to_utf8_buffer() wasm.memory.seek(offset).put_data(bytes) wasm.function("store_data", [offset, bytes.size()]) return bytes.size() func store_int(value: int, offset: int) -> void: var bytes := PackedByteArray() bytes.append(value & 0xFF) bytes.append((value >> 8) & 0xFF) bytes.append((value >> 16) & 0xFF) bytes.append((value >> 24) & 0xFF) wasm.memory.seek(offset).put_data(bytes) func store_bytes(bytes: PackedByteArray, offset := 0) -> int: wasm.memory.seek(offset).put_data(bytes) wasm.function("store_data", [offset, bytes.size()]) return bytes.size()
各データフォーマットの形式によって前処理が必要となりますが、基本バイト列として保存します。
JSONデータも、store_string()を使用してUTF-8エンコードされた文字列として格納されます。
データを保存した後、get_data_ptrを使ってWasmからそのポインタを取得し、メモリから読み出します。
Godot-side
func get_string(text: String) -> String: var offset := 0 var length = store_string(text, offset) var ptr = wasm.function("get_data_ptr") var result = wasm.memory.seek(ptr).get_data(length) return get_result_string(result)
#[unsafe(no_mangle)] pub unsafe extern "C" fn store_data(ptr: *const u8, len: usize) { let data = unsafe { std::slice::from_raw_parts(ptr, len) }; let mut guard = BUFFER.lock(); *guard = Some(data.to_vec()); } #[unsafe(no_mangle)] pub unsafe extern "C" fn get_data_ptr() -> *const u8 { let guard = BUFFER.lock(); guard.as_ref().map(|buf| buf.as_ptr()).unwrap_or(core::ptr::null()) }
Rustのバージョンによって厳格化の違いがありました。
2021editionでは、 #[no_mangle] で良かったのが、2024editionでは、#[unsafe(no_mangle)]とunsafeが求められるし、pub unsafe extern “C” fnと関数にunsafe記述があれば、関数内にはunsafe記述記述が必要なかったのに、2024editionでは、内部の unsafe な操作は明示的な unsafe { ... }
ブロックに入れないといけなくなり、unsafe { std::slice::from_raw_parts(ptr, len) };と必要になります。
また、store処理も、以下の様にできていたのが、
#[no_mangle] pub unsafe extern "C" fn store_data(ptr: *const u8, len: usize) { let data = std::slice::from_raw_parts(ptr, len); BUFFER = Some(data.to_vec()); }
lazy_staticを用いて、BUFFERの定義と初期化が必要となり、BUFFER へのアクセスは必ず .lock() を使って排他制御しなければいけなくなりました。
lazy_static! { static ref BUFFER: Mutex<Option<Vec<u8>>> = Mutex::new(None); static ref RESULT_BUFFER: Mutex<Option<Vec<u8>>> = Mutex::new(None); }
このように lazy_static!
マクロを用いることで、グローバルな Mutex<Option<Vec<u8>>>
を安全に初期化できます。
#[unsafe(no_mangle)] pub unsafe extern "C" fn store_data(ptr: *const u8, len: usize) { let data = unsafe { std::slice::from_raw_parts(ptr, len) }; let mut guard = BUFFER.lock(); *guard = Some(data.to_vec()); }
BUFFER
は Mutex
でラップされているため、アクセスするには .lock()
を使ってロックを取得し、排他制御を行う必要があります。これはスレッド間でデータ競合を防ぐために必要です。
と、本題と逸れたRustの仕様になりましたが、ちょっとハマった点でした。
元に戻ると、
入力データに対して変換を行う際は、(例えば文字列を反転させる)、別のバッファとして「RESULT_BUFFER」を使用しています。
lazy_static! { static ref BUFFER: Mutex<Option<Vec<u8>>> = Mutex::new(None); static ref RESULT_BUFFER: Mutex<Option<Vec<u8>>> = Mutex::new(None); } #[unsafe(no_mangle)] pub extern "C" fn reverse_string() { let buffer_guard = BUFFER.lock(); let mut result_guard = RESULT_BUFFER.lock(); if let Some(ref data) = *buffer_guard { if let Ok(input_str) = std::str::from_utf8(data) { let reversed: String = input_str.chars().rev().collect(); *result_guard = Some(reversed.into_bytes()); } else { *result_guard = Some("invalid_utf8".as_bytes().to_vec()); } } else { *result_guard = Some("no_data".as_bytes().to_vec()); } }
この結果を取得するには、以下の関数でポインタを取得
#[unsafe(no_mangle)] pub unsafe extern "C" fn get_result_buffer_ptr() -> *const u8 { let guard = RESULT_BUFFER.lock(); guard.as_ref().map(|buf| buf.as_ptr()).unwrap_or(core::ptr::null()) }
Godot側では、処理後に変換されたデータを取り出すことができます。
func get_string_reverse(text: String) -> String: var offset := 0 var length = store_string(text, offset) wasm.function("reverse_string") var ptr = wasm.function("get_result_buffer_ptr") var result = wasm.memory.seek(ptr).get_data(length) return get_result_string(result)
処理の流れをまとめますと、
- store_* を使ってデータを Wasm メモリに転送する。
- 生の入力を取得するには get_data_ptr() を使用します。
- 処理結果を取得するには get_result_buffer_ptr() を使用します。
- BUFFERは生の入力用、RESULT_BUFFERは変換用
- JSONは文字列として扱われ、取得後に解析される。
と言った形で、様々なデータフォーマットをWASMに送信・取得する方法でした。
ではでは。
またまたぁ。