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

Archives Details

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

Godot

2025.07.06

この記事は最終更新日から1年以上が経過しています。

どもです。

前回「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

ラズパイが高騰しているのでストレス解消のためAliExpressで購入した電子パーツ達

電子工作

2026.08.02

【2026】【VMware Fusion】MacでWindowsを無料で扱う。

windows

2026.07.19

Raspberry Pi 5 でマインクラフトサーバーを立てる(Java版 × 統合版クロスプレイ対応)

RaspberryPi

2026.06.24

ラズパイが高い。

RaspberryPi

2026.05.26

【Claude Code】フル稼働。ToDo Appを様々なGUIフレームワーク用いて作らせる。

AI・Bot・algorithm

2026.05.24

Macで歩く「たのしいバイナリの歩き方」うさみみハリケーンの代わりに、Cheat Engine / Bit slicerを使用する

アセンブラ

2026.04.12

Macで歩く「たのしいバイナリの歩き方」

アセンブラ

2026.04.10

【Railway】ひたすら安く個人開発サービスを運用する計画

サーバー

2026.04.06

たびのきろく

イベント

2026.02.23

【Railway】MySQLサービスをコスト抑えて運用する

運用

2026.01.19

あけましておめでとうございますmm DjangoアプリをRailwayに移行する。

運用

2026.01.06

効率の良い AI駆動開発について考える

AI・Bot・algorithm

2025.11.09

RANKING

Follow

SPONSOR

現在、掲載募集中です。



Links

About Us

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

Entry Profile

Graphical FrontEnd Engineer
- Daisuke Takayama

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

FOLLOW US