@@ -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
15971629static 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') */
15981634static 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);
29262962static void statement_join(char **p);
29272963static void statement_open(char **p);
29282964static void statement_close(char **p);
2965+ static void close_channel(int lfn);
29292966static void statement_download(char **p);
29302967static 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+
1359813690static 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