このサイトは、只今WEB業界で活躍中のデザイナー、プログラマーの方々の情報を集めたweb統合情報サイトです。

Archives Details

Godot 4 & WebAssemblyで様々なデータフォーマットを処理

Godot

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 から返されるメモリポインタを使用して取得する形となります。

Godot-side: Store Functions

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)

Rust-side: Wasm Data Handling

#[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());
}

BUFFERMutex でラップされているため、アクセスするには .lock() を使ってロックを取得し、排他制御を行う必要があります。これはスレッド間でデータ競合を防ぐために必要です。

と、本題と逸れたRustの仕様になりましたが、ちょっとハマった点でした。

元に戻ると、

Rust-side: Processing with Result Buffer

入力データに対して変換を行う際は、(例えば文字列を反転させる)、別のバッファとして「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-side: Read Transformed Result

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

ではでは。

またまたぁ。

Comment

Related Article

Godot 4 & WebAssemblyで様々なデータフォーマットを処理

2025.07.06

Godot 4 & WebAssemblyで、Hello WebAssembly! – godot-wasm

2025.06.21

CATEGORY LIST

LATEST NEWS

Godot 4 & WebAssemblyで様々なデータフォーマットを処理

Godot

2025.07.06

Godot 4 & WebAssemblyで、Hello WebAssembly! - godot-wasm

Godot

2025.06.21

Godot 4 & Rust 始め方

Rust

2025.06.19

ご、ごめん。。今はFlutterの気分じゃないんだ。。

Flutter

2025.05.30

AIのために働け。AI リーダブルコーディングな未来。

AI・Bot・algorithm

2025.05.21

Rustでつくる ふっかつのじゅもん

Rust

2025.04.29

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その5

Rust

2025.04.14

う、動くぞ! Mac mini Apple M4 Pro で PS3ソフトを遊ぶ。RPCS3 Mac版を起動

Game

2025.04.12

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その4

Rust

2025.04.10

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その3

Rust

2025.04.08

Tauri(Rust) × AI で作る GitGUIクライアントアプリ その2

Rust

2025.04.07

時代を先取りし過ぎた ニューラルネットワークが導入されたゲーム『がんばれ森川君2号』を令和に嗜んでみる。

Game

2025.04.06

RANKING

Follow

SPONSOR

現在、掲載募集中です。



Links

About Us

WEBデザイナーの、WEBデザイナーによる、WEBデザイナーの為のサイト。「みんなで書こう!」と仲間を募ってみたが、結局書くのは自分だけとなってしまいました。日々のメモを綴っていきます。

Entry Profile

Graphical FrontEnd Engineer
- Daisuke Takayama

MAD CITY 北九州市で生まれ育つ。20代はバンド活動に明け暮れ、ふと「webデザイナーになりたい。」と思い、デジタルハリウッド福岡校入学。卒業後、数々の賞を受賞、web業界をざわつかせる。
現在、主に、ゲーム制作中心に港区六本木界隈で活動中。

FOLLOW US