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に送信・取得する方法でした。
ではでは。
またまたぁ。










