Using the swift programming language on Playdate

Had a random shower thought: why not get a WASM runtime up and running instead & then just use SwiftWASM. WAMR based, going to see how hard adding playdate as a platform would be. WASM on PD would be neat in general.

Edit: Bidirectional communication works okay enough, definitely a bit quirky. I would love to know what this runs like on device :laughing: it is a 9mb wasm file.

nice

The swift code from above:

import api

var i: Int32 = 0

@_cdecl("updateCallback")
func updateCallback() -> Bool {
  i += add(1,2) // external c func
  let _ = "hello swift wasm: \(i)".withCString {
    // external c func that draws the text on screen
    // ugly because there is something up with `const char*`
    strtest(UnsafeMutablePointer(mutating: $0))
  }
  return true
}

and the c code is actually pretty tame:

#include <stdio.h>

#include "pd_api.h"
#include "wasm_export.h"

int bridgeUpdateCallback(void *);
char *bh_read_file_to_buffer(const char *filename, uint32_t *ret_size);

wasm_exec_env_t exec_env;
wasm_module_inst_t module_inst;
wasm_function_inst_t func;

void add(wasm_exec_env_t exec_env, uint64_t *args) {
    native_raw_return_type(int, args);
    native_raw_get_arg(int, x, args);
    native_raw_get_arg(int, y, args);
    int res = x + y;
    native_raw_set_return(res);
}

void strtest(wasm_exec_env_t exec_env, uint64_t *args) {
    native_raw_get_arg(char*, x, args);
    pd->graphics->clear(kColorWhite);
    pd->graphics->drawText(x, 24, kUTF8Encoding, 40, 200);
}

#ifdef _WINDLL
__declspec(dllexport)
#endif

static NativeSymbol native_symbols[] = {
        EXPORT_WASM_API_WITH_SIG(add, "(ii)i"),
        EXPORT_WASM_API_WITH_SIG(strtest, "($)")
};

int eventHandler(PlaydateAPI *playdate, PDSystemEvent event, uint32_t arg) {
    (void) arg;

    if (event == kEventInit) {
        pd = playdate;

        char *buffer, error_buf[128];
        wasm_module_t module;
        uint32_t buf_size, stack_size = 8092 * 2, heap_size = 8092 * 2;

        /* initialize the wasm runtime by default configurations */
        wasm_runtime_init();

        buffer = bh_read_file_to_buffer("swift-wasm.wasm", &buf_size);
        if (!buffer) {
            pd->system->error("Open wasm app file failed.");
        }

        int n_native_symbols = sizeof(native_symbols) / sizeof(NativeSymbol);
        if (!wasm_runtime_register_natives_raw("env",
                                           native_symbols,
                                           n_native_symbols)) {
            pd->system->error("Injecting api failed. error: %s", error_buf);
        }

        module = wasm_runtime_load(buffer, buf_size, error_buf, sizeof(error_buf));
        if (!module) {
            pd->system->error("Load wasm module failed. error: %s", error_buf);
        }

        module_inst = wasm_runtime_instantiate(module, stack_size, heap_size,
                                               error_buf, sizeof(error_buf));

        if (!module_inst) {
            pd->system->error("Instantiate wasm module failed. error: %s", error_buf);
        }

        exec_env = wasm_runtime_create_exec_env(module_inst, stack_size);
        if (!exec_env) {
            pd->system->error("Create wasm execution environment failed.\n");
        }

        if (!(func = wasm_runtime_lookup_function(module_inst, "updateCallback", NULL))) {
            pd->system->error("The updateCallback wasm function is not found.\n");
        }

        pd->display->setRefreshRate(0);
        pd->system->setUpdateCallback(bridgeUpdateCallback, NULL);
    }

    return 0;
}

int bridgeUpdateCallback(void *userdata) {
    uint32_t argv[1];
    if (!wasm_runtime_call_wasm(exec_env, func, 0, argv)) {
        pd->system->error("call wasm function bridgeUpdate failed. %s\n", wasm_runtime_get_exception(module_inst));
    }
    int ret_val = argv[0];
    return ret_val;
}

And all of this is based on the WAMR runtime, which is optimized for small chips / memory use cases. Creating this demo took a little longer than originally anticipated since I also went ahead and properly added playdate as a platform for WAMR :smiley: upstream soon! This isn't limited to swift either, would be exciting to see what else could be done.

Edit 2: Slightly off topic, but if anyone is interested in a proper toolchain cmake setup, I can open source. Looks like this:

cmake_minimum_required(VERSION 3.14)
set(CMAKE_C_STANDARD 11)

set(PLAYDATE_GAME_NAME WASM)
project(${PLAYDATE_GAME_NAME} C ASM)

set (WAMR_BUILD_PLATFORM "playdate")
set (WAMR_ROOT_DIR wamr)
set (WAMR_BUILD_INTERP 1)
set (WAMR_BUILD_FAST_INTERP 1)
set (WAMR_BUILD_AOT 0)
set (WAMR_BUILD_LIBC_BUILTIN 0)
set (WAMR_BUILD_LIBC_WASI 1)
set (WAMR_BUILD_INVOKE_NATIVE_GENERAL 1) # skip the arm files
set (WAMR_BUILD_SHARED_MEMORY 0)
set (WASM_ENABLE_THREAD_MGR 0)
set (WASM_ENABLE_APP_FRAMEWORK 0)
set (WAMR_DISABLE_APP_ENTRY 1)

include (${WAMR_ROOT_DIR}/build-scripts/runtime_lib.cmake)

playdate_add_executable(${PLAYDATE_GAME_NAME} main.c ${WAMR_RUNTIME_LIB_SOURCE})

Consists of 2 toolchains, host and device, you just switch the one you are using and it will build for that platform + pdc. The other really nice thing is the device toolchain also automatically finds the arm toolchain.

2 Likes