Skip to content

Commit bce4f4e

Browse files
committed
feat(io): forward CLOSE-after-write to host via wasm_js_host_file_write
Adds an EM_JS hook that fires when the BASIC interpreter closes a channel that was OPENed in write or append mode (secondary 1 / 2 or mode-prefix 'w' / 'a'). Slurps the file's bytes off MEMFS after the fclose so the OS-side flush is durable, then calls Module.rgcHostFileWrite(path, bytes) — the iframe in 8bitworkshop forwards the call to the parent page, which persists the file into the project workspace. Without this, BASIC OPEN/PRINT#/CLOSE only writes into the per-run MEMFS scratch, so e.g. `OPEN 1,1,1,"map.json" : PRINT# … : CLOSE 1` in the map editor vanished on next run and was invisible to other programs (rpg.bas, shooter.bas) that MAPLOAD the same path. Implementation: - track open_paths[lfn] (strdup at OPEN, free at CLOSE) and open_modes[lfn] ('r' | 'w' | 'a'). - close_channel(lfn) helper centralises fclose + write-mode forwarding + path free, called from CLOSE and OPEN's re-OPEN branch so an immediate re-OPEN of the same lfn flushes first. - close_forward_to_host() reads the file back via fopen+fread (16 MiB sanity cap) and dispatches the EM_JS hook. - non-emscripten builds get a no-op stub so the call site stays unconditional. Native + terminal builds verified to round-trip via OPEN+PRINT#.
1 parent d6dc963 commit bce4f4e

1 file changed

Lines changed: 97 additions & 11 deletions

File tree

basic.c

Lines changed: 97 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,29 @@ EM_ASYNC_JS(int, wasm_js_http_fetch_to_file_async, (const char *url, const char
136136
return 0;
137137
});
138138

139+
/* Host hook for write-file persistence (browser only). When the BASIC
140+
* program CLOSEs a channel that was opened for write/append, we slurp
141+
* the path's bytes from MEMFS and call Module.rgcHostFileWrite(path,
142+
* data) so the iframe can postMessage them to the IDE workspace. The
143+
* map editor relies on this to make `OPEN 1,1,1,"map.json"` durable
144+
* across runs. No-op if the host didn't register the hook. */
145+
EM_JS(void, wasm_js_host_file_write, (const char *path_utf8, const char *data_ptr, int data_len), {
146+
try {
147+
var M = typeof Module !== 'undefined' ? Module : null;
148+
var fn = M && (typeof M['rgcHostFileWrite'] === 'function' ? M['rgcHostFileWrite']
149+
: (typeof M['onRgcFileWrite'] === 'function' ? M['onRgcFileWrite'] : null));
150+
if (!fn) return;
151+
var path = UTF8ToString(path_utf8);
152+
/* Copy a fresh Uint8Array so the host can keep the bytes after we
153+
* return — HEAPU8 is a view that gets clobbered when wasm grows. */
154+
var bytes = new Uint8Array(data_len);
155+
bytes.set(HEAPU8.subarray(data_ptr, data_ptr + data_len));
156+
fn(path, bytes);
157+
} catch (e) {
158+
/* Swallow — host bug must not crash the BASIC interpreter. */
159+
}
160+
});
161+
139162
/*
140163
* Host hook for EXEC$ / SYSTEM (browser only). Calls Module.rgcHostExec(cmd) or
141164
* Module.onRgcExec(cmd) if function; otherwise EXEC$ -> "" and SYSTEM -> -1.
@@ -188,6 +211,15 @@ EM_ASYNC_JS(int, wasm_js_host_exec_async, (const char *cmd_utf8, char *out, int
188211
});
189212
#endif
190213

214+
#ifndef __EMSCRIPTEN__
215+
/* Native + non-emscripten WASM: no IDE workspace to write back to.
216+
* The CLOSE-after-write hook still calls this, so provide a no-op. */
217+
static void wasm_js_host_file_write(const char *path, const char *data, int len)
218+
{
219+
(void)path; (void)data; (void)len;
220+
}
221+
#endif
222+
191223
#if (defined(__unix__) || defined(__linux__) || defined(__APPLE__) || defined(__MACH__)) && !defined(__EMSCRIPTEN__)
192224
#include <unistd.h>
193225
#include <fcntl.h>
@@ -1595,6 +1627,10 @@ static int data_line_start_index[MAX_DATA_LINES];
15951627
* ST (status) is updated after INPUT#/GET#: 0=ok, 64=EOF, 1=error/not open. */
15961628
#define MAX_OPEN_FILES 16
15971629
static FILE *open_files[256]; /* 1-based; [0] unused; NULL = closed */
1630+
/* Path + mode tracking per channel so CLOSE can forward write-mode
1631+
* files to the host (browser persists them into the IDE workspace). */
1632+
static char *open_paths[256]; /* strdup'd at OPEN, free()'d at CLOSE */
1633+
static char open_modes[256]; /* 'r' | 'w' | 'a' (raw-mode prefix collapsed to 'w'/'r') */
15981634
static void set_io_status(int st);
15991635

16001636
/* BUFFER type — RAM-disk files for large payloads (HTTP responses, JSON, etc.)
@@ -2926,6 +2962,7 @@ static void statement_split(char **p);
29262962
static void statement_join(char **p);
29272963
static void statement_open(char **p);
29282964
static void statement_close(char **p);
2965+
static void close_channel(int lfn);
29292966
static void statement_download(char **p);
29302967
static void statement_mapload(char **p);
29312968
#ifdef GFX_VIDEO
@@ -13551,9 +13588,11 @@ static void statement_open(char **p)
1355113588
}
1355213589
}
1355313590
}
13554-
if (open_files[lfn]) {
13555-
fclose(open_files[lfn]);
13556-
open_files[lfn] = NULL;
13591+
/* Re-OPEN same lfn: close the old channel first, including any
13592+
* write-mode forwarding, so a re-OPEN flushes the previous write
13593+
* back to the host workspace before we redirect the channel. */
13594+
if (open_files[lfn] || open_paths[lfn]) {
13595+
close_channel(lfn);
1355713596
}
1355813597
if (fname[0] == '\0') {
1355913598
runtime_error_hint("OPEN: filename required",
@@ -13586,6 +13625,16 @@ static void statement_open(char **p)
1358613625
return;
1358713626
}
1358813627
open_files[lfn] = fp;
13628+
/* Track path + mode so CLOSE can forward write/append output to
13629+
* the host (browser persists into the IDE workspace). Free any
13630+
* stale strdup from a previous OPEN on this lfn. */
13631+
if (open_paths[lfn]) { free(open_paths[lfn]); open_paths[lfn] = NULL; }
13632+
open_paths[lfn] = strdup(fname);
13633+
if (mode && (mode[0] == 'w' || mode[0] == 'a')) {
13634+
open_modes[lfn] = mode[0];
13635+
} else {
13636+
open_modes[lfn] = 'r';
13637+
}
1358913638
set_io_status(0);
1359013639
} else {
1359113640
set_io_status(1);
@@ -13595,27 +13644,64 @@ static void statement_open(char **p)
1359513644
}
1359613645

1359713646
/* CLOSE [lfn [, lfn ...]] or CLOSE (close all) */
13647+
/* Slurp the file at path and call wasm_js_host_file_write so the host
13648+
* can persist it (browser IDE workspace). Caller has already fclose'd
13649+
* the channel so the OS-side flush is durable. No-op if the file
13650+
* doesn't exist or the path is missing. */
13651+
static void close_forward_to_host(const char *path)
13652+
{
13653+
FILE *rfp;
13654+
long len;
13655+
char *buf;
13656+
if (!path || !path[0]) return;
13657+
rfp = fopen(path, "rb");
13658+
if (!rfp) return;
13659+
fseek(rfp, 0, SEEK_END);
13660+
len = ftell(rfp);
13661+
fseek(rfp, 0, SEEK_SET);
13662+
if (len < 0) { fclose(rfp); return; }
13663+
if (len > 16 * 1024 * 1024) { fclose(rfp); return; } /* sanity cap */
13664+
buf = (char *)malloc((size_t)len);
13665+
if (!buf) { fclose(rfp); return; }
13666+
if (len > 0 && fread(buf, 1, (size_t)len, rfp) != (size_t)len) {
13667+
fclose(rfp); free(buf); return;
13668+
}
13669+
fclose(rfp);
13670+
wasm_js_host_file_write(path, buf, (int)len);
13671+
free(buf);
13672+
}
13673+
13674+
static void close_channel(int lfn)
13675+
{
13676+
int forward;
13677+
if (lfn < 1 || lfn > 255) return;
13678+
forward = (open_modes[lfn] == 'w' || open_modes[lfn] == 'a');
13679+
if (open_files[lfn]) {
13680+
fclose(open_files[lfn]);
13681+
open_files[lfn] = NULL;
13682+
}
13683+
if (forward) {
13684+
close_forward_to_host(open_paths[lfn]);
13685+
}
13686+
if (open_paths[lfn]) { free(open_paths[lfn]); open_paths[lfn] = NULL; }
13687+
open_modes[lfn] = 0;
13688+
}
13689+
1359813690
static void statement_close(char **p)
1359913691
{
1360013692
int lfn;
1360113693
skip_spaces(p);
1360213694
if (**p == '\0' || **p == ':') {
1360313695
for (lfn = 1; lfn < 256; lfn++) {
13604-
if (open_files[lfn]) {
13605-
fclose(open_files[lfn]);
13606-
open_files[lfn] = NULL;
13607-
}
13696+
if (open_files[lfn] || open_paths[lfn]) close_channel(lfn);
1360813697
}
1360913698
return;
1361013699
}
1361113700
for (;;) {
1361213701
if (!isdigit((unsigned char)**p)) break;
1361313702
lfn = atoi(*p);
1361413703
while (isdigit((unsigned char)**p)) (*p)++;
13615-
if (lfn >= 1 && lfn <= 255 && open_files[lfn]) {
13616-
fclose(open_files[lfn]);
13617-
open_files[lfn] = NULL;
13618-
}
13704+
close_channel(lfn);
1361913705
skip_spaces(p);
1362013706
if (**p != ',') break;
1362113707
(*p)++;

0 commit comments

Comments
 (0)