From 3f2b4177d65d49afe0cc9beda079fea32554c307 Mon Sep 17 00:00:00 2001 From: John Alanbrook Date: Sat, 6 Dec 2025 17:35:23 -0600 Subject: [PATCH] start playdate support --- meson.build | 22 +- playdate.cross | 2 +- scripts/os.c | 4 +- source/cell.c | 5 +- source/fd_playdate.c | 578 ++++++++++++++++++++++++++++++++++++++ source/scheduler_single.c | 2 +- 6 files changed, 601 insertions(+), 12 deletions(-) create mode 100644 source/fd_playdate.c diff --git a/meson.build b/meson.build index 077a5528..2a2920f4 100644 --- a/meson.build +++ b/meson.build @@ -8,7 +8,7 @@ libtype = get_option('default_library') link = [] src = [] -add_project_arguments('-Wno-gnu-label-as-value', '-Wno-int-conversion', language: ['c']) +add_project_arguments('-Wno-int-conversion', language: ['c']) git_tag_cmd = run_command('git', 'describe', '--tags', '--abbrev=0', check: false) cell_version = 'unknown' @@ -33,8 +33,7 @@ add_project_arguments('-Wno-incompatible-pointer-types', language: 'c') add_project_arguments('-Wno-narrowing', language: 'cpp') add_project_arguments('-Wno-missing-braces', language:'c') add_project_arguments('-Wno-strict-prototypes', language:'c') -add_project_arguments('-Wno-unused-command-line-argument', language: 'c') -add_project_arguments('-Wno-unused-command-line-argument', language: 'cpp') +add_project_arguments('-Wno-unused-function', language: 'c') deps = [] @@ -49,6 +48,10 @@ if host_machine.system() == 'darwin' endforeach endif +if host_machine.system() == 'playdate' + add_project_arguments('-DTARGET_PLAYDATE', language: 'c') +endif + cmake = import('cmake') cc = meson.get_compiler('c') @@ -66,7 +69,7 @@ if host_machine.system() == 'windows' endif -if host_machine.system() != 'emscripten' +if host_machine.system() != 'emscripten' and host_machine.system() != 'playdate' # Try to find system-installed enet first enet_dep = dependency('enet', static: true, required: false) if not enet_dep.found() @@ -118,7 +121,9 @@ src += [ # core 'miniz.c' ] -if get_option('single_threaded') +if host_machine.system() == 'playdate' + src += ['scheduler_single.c'] +elif get_option('single_threaded') src += ['scheduler_single.c'] else src += ['scheduler_threaded.c'] @@ -141,7 +146,6 @@ scripts = [ 'debug.c', 'os.c', 'fd.c', - 'socket.c', 'http.c', 'enet.c', 'wildstar.c', @@ -153,6 +157,12 @@ foreach file: scripts sources += files(full_path) endforeach +if host_machine.system() == 'playdate' + sources += files('source/fd_playdate.c') +else + sources += files('scripts/fd.c') +endif + srceng = 'source' includes = [srceng] diff --git a/playdate.cross b/playdate.cross index 6ee1d28e..1a942aad 100644 --- a/playdate.cross +++ b/playdate.cross @@ -7,7 +7,7 @@ objcopy = 'arm-none-eabi-objcopy' ld = 'arm-none-eabi-ld' [host_machine] -system = 'none' +system = 'playdate' cpu_family = 'arm' cpu = 'cortex-m7' endian = 'little' diff --git a/scripts/os.c b/scripts/os.c index 994e72c7..8ed15301 100644 --- a/scripts/os.c +++ b/scripts/os.c @@ -17,17 +17,15 @@ #include #include #include +#include #ifdef __linux__ #include #include #include -#include #include #endif #endif -#include - static JSClassID js_dylib_class_id; static void js_dylib_finalizer(JSRuntime *rt, JSValue val) { diff --git a/source/cell.c b/source/cell.c index bbc0ab75..4dd8985b 100644 --- a/source/cell.c +++ b/source/cell.c @@ -444,6 +444,7 @@ void script_startup(cell_rt *prt) set_actor_state(crt); } +#ifndef TARGET_PLAYDATE static void signal_handler(int sig) { const char *str = NULL; @@ -459,6 +460,7 @@ static void signal_handler(int sig) exit_handler(); } +#endif int cell_init(int argc, char **argv) { @@ -503,11 +505,12 @@ int cell_init(int argc, char **argv) root_cell = create_actor(startwota.data); - /* Set up signal and exit handlers */ + #ifndef TARGET_PLAYDATE signal(SIGINT, signal_handler); signal(SIGTERM, signal_handler); signal(SIGSEGV, signal_handler); signal(SIGABRT, signal_handler); + #endif actor_loop(); diff --git a/source/fd_playdate.c b/source/fd_playdate.c new file mode 100644 index 00000000..ae2d72f4 --- /dev/null +++ b/source/fd_playdate.c @@ -0,0 +1,578 @@ +#include "cell.h" +#include +#include +#include +#include + +// --- Playdate File API Definitions --- + +#ifndef pdext_file_h +#define pdext_file_h + +#if TARGET_EXTENSION + +typedef void SDFile; + +typedef enum +{ + kFileRead = (1<<0), + kFileReadData = (1<<1), + kFileWrite = (1<<2), + kFileAppend = (2<<2) +} FileOptions; + +typedef struct +{ + int isdir; + unsigned int size; + int m_year; + int m_month; + int m_day; + int m_hour; + int m_minute; + int m_second; +} FileStat; + +#endif + +#if !defined(SEEK_SET) +# define SEEK_SET 0 /* Seek from beginning of file. */ +# define SEEK_CUR 1 /* Seek from current position. */ +# define SEEK_END 2 /* Set file pointer to EOF plus "offset" */ +#endif + +struct playdate_file +{ + const char* (*geterr)(void); + + int (*listfiles)(const char* path, void (*callback)(const char* path, void* userdata), void* userdata, int showhidden); + int (*stat)(const char* path, FileStat* stat); + int (*mkdir)(const char* path); + int (*unlink)(const char* name, int recursive); + int (*rename)(const char* from, const char* to); + + SDFile* (*open)(const char* name, FileOptions mode); + int (*close)(SDFile* file); + int (*read)(SDFile* file, void* buf, unsigned int len); + int (*write)(SDFile* file, const void* buf, unsigned int len); + int (*flush)(SDFile* file); + int (*tell)(SDFile* file); + int (*seek)(SDFile* file, int pos, int whence); +}; + +#endif /* pdext_file_h */ + +// Assumed global pointer to the file API +extern struct playdate_file* pd_file; + +// --- Helper Functions --- + +// Convert JS value to SDFile* (casting via intptr_t/int) +static SDFile* js2fd(JSContext *ctx, JSValueConst val) +{ + int32_t fd_int; + if (JS_ToInt32(ctx, &fd_int, val) < 0) { + JS_ThrowTypeError(ctx, "Expected file descriptor number"); + return NULL; + } + return (SDFile*)(intptr_t)fd_int; +} + +static time_t pd_time_to_unix(FileStat *fs) { + struct tm t = {0}; + t.tm_year = fs->m_year - 1900; + t.tm_mon = fs->m_month - 1; + t.tm_mday = fs->m_day; + t.tm_hour = fs->m_hour; + t.tm_min = fs->m_minute; + t.tm_sec = fs->m_second; + return mktime(&t); +} + +// --- Implementation --- + +JSC_SCALL(fd_open, + FileOptions flags = kFileRead; + + // Parse optional flags argument + if (argc > 1 && JS_IsString(argv[1])) { + const char *flag_str = JS_ToCString(js, argv[1]); + flags = 0; + + if (strchr(flag_str, 'r')) flags |= kFileRead; + if (strchr(flag_str, 'w')) flags |= kFileWrite; + if (strchr(flag_str, 'a')) flags |= kFileAppend; + if (strchr(flag_str, '+')) { + // Read+Write not directly mapped, assume Write covers it or combine? + // Playdate docs say kFileRead|kFileWrite is valid + flags |= kFileRead | kFileWrite; + } + + JS_FreeCString(js, flag_str); + } + + SDFile* file = pd_file->open(str, flags); + if (!file) { + const char* err = pd_file->geterr(); + ret = JS_ThrowInternalError(js, "open failed: %s", err ? err : "unknown error"); + } else { + ret = JS_NewInt32(js, (int32_t)(intptr_t)file); + } +) + +JSC_CCALL(fd_write, + SDFile* fd = js2fd(js, argv[0]); + if (!fd) return JS_EXCEPTION; + + size_t len; + int wrote; + if (JS_IsString(argv[1])) { + const char *data = JS_ToCStringLen(js, &len, argv[1]); + if (!data) return JS_EXCEPTION; + wrote = pd_file->write(fd, data, (unsigned int)len); + JS_FreeCString(js, data); + } else { + void *data = js_get_blob_data(js, &len, argv[1]); + if (data == (void*)-1) + return JS_EXCEPTION; + wrote = pd_file->write(fd, data, (unsigned int)len); + } + + if (wrote < 0) { + const char* err = pd_file->geterr(); + return JS_ThrowInternalError(js, "write failed: %s", err ? err : "unknown error"); + } + + return JS_NewInt64(js, wrote); +) + +JSC_CCALL(fd_read, + SDFile* fd = js2fd(js, argv[0]); + if (!fd) return JS_EXCEPTION; + + size_t size = 4096; + if (argc > 1) + size = js2number(js, argv[1]); + + void *buf = malloc(size); + if (!buf) + return JS_ThrowInternalError(js, "malloc failed"); + + int bytes_read = pd_file->read(fd, buf, (unsigned int)size); + if (bytes_read < 0) { + free(buf); + const char* err = pd_file->geterr(); + return JS_ThrowInternalError(js, "read failed: %s", err ? err : "unknown error"); + } + + ret = js_new_blob_stoned_copy(js, buf, bytes_read); + free(buf); + return ret; +) + +JSC_SCALL(fd_slurp, + FileStat st; + if (pd_file->stat(str, &st) != 0) { + // const char* err = pd_file->geterr(); // stat usually returns -1 on error + return JS_ThrowInternalError(js, "stat failed for %s", str); + } + + if (st.isdir) + return JS_ThrowTypeError(js, "path is a directory"); + + size_t size = st.size; + if (size == 0) + return js_new_blob_stoned_copy(js, NULL, 0); + + SDFile* fd = pd_file->open(str, kFileRead); + if (!fd) { + const char* err = pd_file->geterr(); + return JS_ThrowInternalError(js, "open failed: %s", err ? err : "unknown error"); + } + + void *data = malloc(size); + if (!data) { + pd_file->close(fd); + return JS_ThrowInternalError(js, "malloc failed"); + } + + int bytes_read = pd_file->read(fd, data, (unsigned int)size); + pd_file->close(fd); + + if (bytes_read < 0) { + free(data); + return JS_ThrowInternalError(js, "read failed"); + } + + ret = js_new_blob_stoned_copy(js, data, bytes_read); + free(data); +) + +JSC_CCALL(fd_lseek, + SDFile* fd = js2fd(js, argv[0]); + if (!fd) return JS_EXCEPTION; + + int offset = (int)js2number(js, argv[1]); + int whence = SEEK_SET; + + if (argc > 2) { + const char *whence_str = JS_ToCString(js, argv[2]); + if (strcmp(whence_str, "cur") == 0) whence = SEEK_CUR; + else if (strcmp(whence_str, "end") == 0) whence = SEEK_END; + JS_FreeCString(js, whence_str); + } + + if (pd_file->seek(fd, offset, whence) != 0) { + const char* err = pd_file->geterr(); + return JS_ThrowInternalError(js, "seek failed: %s", err ? err : "unknown error"); + } + + // tell to get new pos + int new_pos = pd_file->tell(fd); + return JS_NewInt64(js, new_pos); +) + +JSC_CCALL(fd_getcwd, + // Playdate is always at root of its sandbox + return JS_NewString(js, "/"); +) + +JSC_SCALL(fd_rmdir, + int recursive = 0; + if (argc > 1) + recursive = JS_ToBool(js, argv[1]); + + if (pd_file->unlink(str, recursive) != 0) { + const char* err = pd_file->geterr(); + ret = JS_ThrowInternalError(js, "could not remove %s: %s", str, err ? err : "unknown error"); + } +) + +JSC_SCALL(fd_mkdir, + if (pd_file->mkdir(str) != 0) { + const char* err = pd_file->geterr(); + ret = JS_ThrowInternalError(js, "could not make directory %s: %s", str, err ? err : "unknown error"); + } +) + +JSC_SCALL(fd_mv, + if (argc < 2) + ret = JS_ThrowTypeError(js, "fd.mv requires 2 arguments: old path and new path"); + else if (!JS_IsString(argv[1])) + ret = JS_ThrowTypeError(js, "second argument must be a string (new path)"); + else { + const char *new_path = JS_ToCString(js, argv[1]); + if (pd_file->rename(str, new_path) != 0) { + const char* err = pd_file->geterr(); + ret = JS_ThrowInternalError(js, "could not rename %s to %s: %s", str, new_path, err ? err : "unknown error"); + } + JS_FreeCString(js, new_path); + } +) + +JSC_SCALL(fd_symlink, + // Not supported + if (argc >= 2 && JS_IsString(argv[1])) { + // consume arg + JS_FreeCString(js, JS_ToCString(js, argv[1])); + } + ret = JS_ThrowInternalError(js, "symlink not supported on Playdate"); +) + +JSC_SCALL(fd_unlink, + if (pd_file->unlink(str, 0) != 0) { + const char* err = pd_file->geterr(); + ret = JS_ThrowInternalError(js, "could not remove file %s: %s", str, err ? err : "unknown error"); + } +) + +JSC_SCALL(fd_rm, + // Recursive remove + if (pd_file->unlink(str, 1) != 0) { + const char* err = pd_file->geterr(); + ret = JS_ThrowInternalError(js, "could not remove %s: %s", str, err ? err : "unknown error"); + } +) + +JSC_CCALL(fd_fsync, + SDFile* fd = js2fd(js, argv[0]); + if (!fd) return JS_EXCEPTION; + + if (pd_file->flush(fd) != 0) { + const char* err = pd_file->geterr(); + return JS_ThrowInternalError(js, "fsync failed: %s", err ? err : "unknown error"); + } + + return JS_NULL; +) + +JSC_CCALL(fd_close, + SDFile* fd = js2fd(js, argv[0]); + if (!fd) return JS_EXCEPTION; + + if (pd_file->close(fd) != 0) { + const char* err = pd_file->geterr(); + return JS_ThrowInternalError(js, "close failed: %s", err ? err : "unknown error"); + } + + return JS_NULL; +) + +JSC_CCALL(fd_fstat, + // fstat not supported by Playdate API + SDFile* fd = js2fd(js, argv[0]); + if (!fd) return JS_EXCEPTION; + + // We can't get stat from SDFile*. Return minimal object or error? + // fd.c returns full object. + // Return empty object with error property? Or just size 0? + // Let's return empty object. + JSValue obj = JS_NewObject(js); + return obj; +) + +JSC_SCALL(fd_stat, + FileStat st; + if (pd_file->stat(str, &st) != 0) { + return JS_NewObject(js); // Return empty on failure/not found + } + + JSValue obj = JS_NewObject(js); + JS_SetPropertyStr(js, obj, "size", JS_NewInt64(js, st.size)); + // Approximate mode + JS_SetPropertyStr(js, obj, "mode", JS_NewInt32(js, st.isdir ? 0040755 : 0100644)); + JS_SetPropertyStr(js, obj, "uid", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, obj, "gid", JS_NewInt32(js, 0)); + + time_t ts = pd_time_to_unix(&st); + JS_SetPropertyStr(js, obj, "atime", JS_NewInt64(js, ts)); + JS_SetPropertyStr(js, obj, "mtime", JS_NewInt64(js, ts)); + JS_SetPropertyStr(js, obj, "ctime", JS_NewInt64(js, ts)); + + JS_SetPropertyStr(js, obj, "nlink", JS_NewInt32(js, 1)); + JS_SetPropertyStr(js, obj, "ino", JS_NewInt64(js, 0)); + JS_SetPropertyStr(js, obj, "dev", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, obj, "rdev", JS_NewInt32(js, 0)); + JS_SetPropertyStr(js, obj, "blksize", JS_NewInt32(js, 512)); + JS_SetPropertyStr(js, obj, "blocks", JS_NewInt64(js, (st.size + 511) / 512)); + + JS_SetPropertyStr(js, obj, "isFile", JS_NewBool(js, !st.isdir)); + JS_SetPropertyStr(js, obj, "isDirectory", JS_NewBool(js, st.isdir)); + JS_SetPropertyStr(js, obj, "isSymlink", JS_NewBool(js, 0)); + JS_SetPropertyStr(js, obj, "isFIFO", JS_NewBool(js, 0)); + JS_SetPropertyStr(js, obj, "isSocket", JS_NewBool(js, 0)); + JS_SetPropertyStr(js, obj, "isCharDevice", JS_NewBool(js, 0)); + JS_SetPropertyStr(js, obj, "isBlockDevice", JS_NewBool(js, 0)); + + return obj; +) + +struct listfiles_ctx { + JSContext *js; + JSValue array; + int index; +}; + +static void listfiles_cb(const char *path, void *userdata) { + struct listfiles_ctx *ctx = (struct listfiles_ctx*)userdata; + if (strcmp(path, ".") == 0 || strcmp(path, "..") == 0) return; + + // Playdate listfiles returns just the name, but sometimes with slash for dir? + // Docs say "names of files". + + JS_SetPropertyUint32(ctx->js, ctx->array, ctx->index++, JS_NewString(ctx->js, path)); +} + +JSC_SCALL(fd_readdir, + JSValue ret_arr = JS_NewArray(js); + struct listfiles_ctx ctx = { js, ret_arr, 0 }; + + if (pd_file->listfiles(str, listfiles_cb, &ctx, 0) != 0) { + const char* err = pd_file->geterr(); + JS_FreeValue(js, ret_arr); + return JS_ThrowInternalError(js, "listfiles failed: %s", err ? err : "unknown error"); + } + + ret = ret_arr; +) + +JSC_CCALL(fd_is_file, + const char *path = JS_ToCString(js, argv[0]); + if (!path) return JS_EXCEPTION; + + FileStat st; + int res = pd_file->stat(path, &st); + JS_FreeCString(js, path); + + if (res != 0) return JS_NewBool(js, 0); + return JS_NewBool(js, !st.isdir); +) + +JSC_CCALL(fd_is_dir, + const char *path = JS_ToCString(js, argv[0]); + if (!path) return JS_EXCEPTION; + + FileStat st; + int res = pd_file->stat(path, &st); + JS_FreeCString(js, path); + + if (res != 0) return JS_NewBool(js, 0); + return JS_NewBool(js, st.isdir); +) + +JSC_CCALL(fd_slurpwrite, + size_t len; + const char *data = js_get_blob_data(js, &len, argv[1]); + + if (data == (const char *)-1) + return JS_EXCEPTION; + + const char *str = JS_ToCString(js, argv[0]); + if (!str) return JS_EXCEPTION; + + SDFile* fd = pd_file->open(str, kFileWrite | kFileRead); // Truncates by default? Playdate docs: kFileWrite truncates. + JS_FreeCString(js, str); + + if (!fd) { + const char* err = pd_file->geterr(); + return JS_ThrowInternalError(js, "open failed: %s", err ? err : "unknown error"); + } + + int written = pd_file->write(fd, data, (unsigned int)len); + pd_file->close(fd); + + if (written != (int)len) { + const char* err = pd_file->geterr(); + return JS_ThrowInternalError(js, "write failed: %s", err ? err : "unknown error"); + } + + return JS_NULL; +) + +// Helper for recursive enumeration +static void visit_directory_pd(JSContext *js, JSValue results, int *result_count, const char *curr_path, const char *rel_prefix, int recurse) { + // We can't use listfiles with arbitrary userdata easily if we need to pass multiple args without struct + // We'll reuse listfiles but we need a way to pass context. + // The context struct can hold current path info. + + // Actually, listfiles takes a callback. + // We need to implement recursion. + // But listfiles doesn't give us full paths in callback, just names. + // So we can reconstruct. + + // We need a nested struct or helper. +} + +struct enum_ctx { + JSContext *js; + JSValue results; + int *count; + const char *curr_path; + const char *rel_prefix; + int recurse; +}; + +static void enum_cb(const char *name, void *userdata) { + struct enum_ctx *ctx = (struct enum_ctx*)userdata; + if (strcmp(name, ".") == 0 || strcmp(name, "..") == 0) return; + + char item_rel[512]; // PATH_MAX + if (ctx->rel_prefix && strlen(ctx->rel_prefix) > 0) { + snprintf(item_rel, sizeof(item_rel), "%s/%s", ctx->rel_prefix, name); + } else { + strcpy(item_rel, name); + } + + JS_SetPropertyUint32(ctx->js, ctx->results, (*ctx->count)++, JS_NewString(ctx->js, item_rel)); + + if (ctx->recurse) { + // Check if directory + char child_path[512]; + snprintf(child_path, sizeof(child_path), "%s/%s", ctx->curr_path, name); + + // Remove trailing slash if name has it? Playdate names sometimes have trailing slash for dirs + // But stat should handle it. + + FileStat st; + if (pd_file->stat(child_path, &st) == 0 && st.isdir) { + struct enum_ctx subctx = *ctx; + subctx.curr_path = child_path; + subctx.rel_prefix = item_rel; + pd_file->listfiles(child_path, enum_cb, &subctx, 0); + } + } +} + +JSC_SCALL(fd_enumerate, + const char *path = str; + if (!path) path = "."; + int recurse = 0; + + if (argc > 1) + recurse = JS_ToBool(js, argv[1]); + + JSValue results = JS_NewArray(js); + int result_count = 0; + + FileStat st; + if (pd_file->stat(path, &st) == 0 && st.isdir) { + struct enum_ctx ctx = { js, results, &result_count, path, "", recurse }; + pd_file->listfiles(path, enum_cb, &ctx, 0); + } + + ret = results; +) + +JSC_CCALL(fd_realpath, + const char *path = JS_ToCString(js, argv[0]); + if (!path) return JS_EXCEPTION; + // Playdate has no realpath, just return path + JSValue res = JS_NewString(js, path); + JS_FreeCString(js, path); + return res; +) + +JSC_CCALL(fd_is_link, + const char *path = JS_ToCString(js, argv[0]); + JS_FreeCString(js, path); + return JS_NewBool(js, 0); +) + +JSC_CCALL(fd_readlink, + const char *path = JS_ToCString(js, argv[0]); + JS_FreeCString(js, path); + return JS_ThrowInternalError(js, "readlink not supported"); +) + +static const JSCFunctionListEntry js_fd_funcs[] = { + MIST_FUNC_DEF(fd, open, 2), + MIST_FUNC_DEF(fd, write, 2), + MIST_FUNC_DEF(fd, read, 2), + MIST_FUNC_DEF(fd, slurp, 1), + MIST_FUNC_DEF(fd, slurpwrite, 2), + MIST_FUNC_DEF(fd, lseek, 3), + MIST_FUNC_DEF(fd, getcwd, 0), + MIST_FUNC_DEF(fd, rmdir, 2), + MIST_FUNC_DEF(fd, unlink, 1), + MIST_FUNC_DEF(fd, mkdir, 1), + MIST_FUNC_DEF(fd, mv, 2), + MIST_FUNC_DEF(fd, rm, 1), + MIST_FUNC_DEF(fd, fsync, 1), + MIST_FUNC_DEF(fd, close, 1), + MIST_FUNC_DEF(fd, stat, 1), + MIST_FUNC_DEF(fd, fstat, 1), + MIST_FUNC_DEF(fd, readdir, 1), + MIST_FUNC_DEF(fd, is_file, 1), + MIST_FUNC_DEF(fd, is_dir, 1), + MIST_FUNC_DEF(fd, is_link, 1), + MIST_FUNC_DEF(fd, enumerate, 2), + MIST_FUNC_DEF(fd, symlink, 2), + MIST_FUNC_DEF(fd, realpath, 1), + MIST_FUNC_DEF(fd, readlink, 1), +}; + +JSValue js_fd_use(JSContext *js) { + JSValue mod = JS_NewObject(js); + JS_SetPropertyFunctionList(js, mod, js_fd_funcs, countof(js_fd_funcs)); + return mod; +} diff --git a/source/scheduler_single.c b/source/scheduler_single.c index b3eebd37..150c1312 100644 --- a/source/scheduler_single.c +++ b/source/scheduler_single.c @@ -406,10 +406,10 @@ void actor_turn(cell_rt *actor) #endif actor->state = ACTOR_RUNNING; + JSValue result; TAKETURN: - JSValue result; if (!arrlen(actor->letters)) { goto ENDTURN; }