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 it is a 9mb wasm file.
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 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.