From d168b659e96693caa92fb2bdd940b53009344043 Mon Sep 17 00:00:00 2001 From: Arnold Date: Mon, 10 Aug 2026 04:21:42 +1000 Subject: [PATCH 1/3] Fix WASM binding resolution across module scopes --- CSharpWasmExpo/main.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/CSharpWasmExpo/main.js b/CSharpWasmExpo/main.js index b8b414d6..2f08bd69 100644 --- a/CSharpWasmExpo/main.js +++ b/CSharpWasmExpo/main.js @@ -9,9 +9,31 @@ const parseMethods = (methods) => { const bindingsFunctions = {}; + const frame = document.querySelector("iframe"); + const wasmScope = frame?.contentWindow; + for (const name of methodList) { try { - bindingsFunctions[name] = eval(name); + let fn = + globalThis[name] ?? + wasmScope?.[name] ?? + wasmScope?.[`_CPP_${name}`]; + + if (typeof fn !== "function") { + const sklibMatch = Object.keys(wasmScope || {}).find( + (key) => key === `__sklib__${name}` || key.startsWith(`__sklib__${name}__`) + ); + + if (sklibMatch && typeof wasmScope[sklibMatch] === "function") { + fn = wasmScope[sklibMatch]; + } + } + + if (typeof fn === "function") { + bindingsFunctions[name] = fn; + } else { + console.warn(`Missing binding: ${name}`); + } } catch (e) { console.warn(e); } @@ -68,4 +90,4 @@ const CompileAndRun = async (code, reportError) => { // This event will be trigger by the csharp compiler document.addEventListener("compileAndRun", (ev) => { CompileAndRun(ev.detail.program[0].source, ev.detail.reportError); -}); +}); \ No newline at end of file From 2e7b5616da88e292b1e628482870fd387da9e5e5 Mon Sep 17 00:00:00 2001 From: Arnold Date: Mon, 7 Sep 2026 05:28:20 +1000 Subject: [PATCH 2/3] Complete C# runtime execution lifecycle contract --- CSharpWasmExpo/main.js | 4 +++ runtimes/csharp/csharpRuntime.js | 46 +++++++++++++++++++++++++++++++- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/CSharpWasmExpo/main.js b/CSharpWasmExpo/main.js index 2f08bd69..1d1a4c23 100644 --- a/CSharpWasmExpo/main.js +++ b/CSharpWasmExpo/main.js @@ -84,6 +84,10 @@ const CompileAndRun = async (code, reportError) => { } } catch (error) { console.error("Error during code execution:", error); + } finally { + // Signal that the run has finished so csharpRuntime.js can report the + // stopped state back to the IDE, re-enabling Run/Restart correctly. + document.dispatchEvent(new CustomEvent("compileAndRunComplete")); } }; diff --git a/runtimes/csharp/csharpRuntime.js b/runtimes/csharp/csharpRuntime.js index 71c48dbe..56c832e4 100644 --- a/runtimes/csharp/csharpRuntime.js +++ b/runtimes/csharp/csharpRuntime.js @@ -1,10 +1,29 @@ // define and create the ExecutionEnvironmentInternal subclass class ExecutionEnvironmentInternalCSharp extends ExecutionEnvironmentInternal { constructor(listenOn) { - return super(listenOn); + super(listenOn); + + // The C# backend runs to completion in a single shot, so it has no + // natural point at which to report that it has finished. main.js + // raises this event once CompileAndRun resolves, which lets us return + // the IDE to the stopped state. + document.addEventListener("compileAndRunComplete", () => { + this.signalStopped(); + }); } async runProgram(program) { + // Report that the program has started. + // + // Without this the parent ExecutionEnvironment never leaves + // ExecutionStatus.Unstarted and hasRunOnce stays false, so the Restart + // and Stop controls are never shown (see updateButtons() in + // javascript/UI/editorMain.js). Every subsequent click then falls + // through the plain Run path, which - unlike restartProgram() - does + // not call cleanEnvironment(), leaving the previous run's canvas in + // place and producing a blank canvas on re-run. + await this.signalStarted(); + const event = new CustomEvent("compileAndRun", { detail: { program: program, @@ -14,6 +33,31 @@ class ExecutionEnvironmentInternalCSharp extends ExecutionEnvironmentInternal { // This event will be handled in the CSharpWasm/main.js file document.dispatchEvent(event); } + + async stopProgram() { + // The C# backend runs synchronously to completion and cannot be + // interrupted part-way through, so this reports the stopped state + // rather than aborting execution. restartProgram() awaits this + // resolving before it calls cleanEnvironment(). + this.signalStopped(); + } + + async pauseProgram() { + // Not supported. The C# backend has no loop-yielding model, so there + // is no safe point at which to suspend it. Kept as a no-op so the + // base class does not throw "Unhandled pauseProgram". + console.warn("pauseProgram is not supported by the C# runtime."); + } + + async continueProgram() { + // See pauseProgram above. + console.warn("continueProgram is not supported by the C# runtime."); + } + + resetExecutionScope() { + // Nothing to tear down on the JS side - loadDotNet() recreates the + // .NET runtime on the next run. + } } let executionEnvironment = null; From 04ce266c4903cdde2f0cacfb867f9a3049d64df9 Mon Sep 17 00:00:00 2001 From: Arnold Date: Sun, 13 Sep 2026 19:55:47 +1000 Subject: [PATCH 3/3] Merge #147 binding diagnostics into #154 --- CSharpWasmExpo/main.js | 96 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 78 insertions(+), 18 deletions(-) diff --git a/CSharpWasmExpo/main.js b/CSharpWasmExpo/main.js index 1d1a4c23..05e84689 100644 --- a/CSharpWasmExpo/main.js +++ b/CSharpWasmExpo/main.js @@ -1,44 +1,86 @@ import { dotnet } from "./wwwroot/_framework/dotnet.js"; import methods from "./splashKitMethods.generated.js"; +// Resolve a single SplashKit binding by name. The generated method list uses +// the plain SplashKit names, but the build can expose them under a few +// different shapes depending on which backend produced them, so each known +// shape is tried in turn. +const resolveBinding = (name, wasmScope) => { + let fn = + globalThis[name] ?? + wasmScope?.[name] ?? + wasmScope?.[`_CPP_${name}`] ?? + globalThis[`__sko_${name}`] ?? + wasmScope?.[`__sko_${name}`]; + + if (typeof fn !== "function") { + const sklibMatch = Object.keys(wasmScope || {}).find( + (key) => key === `__sklib__${name}` || key.startsWith(`__sklib__${name}__`) + ); + + if (sklibMatch && typeof wasmScope[sklibMatch] === "function") { + fn = wasmScope[sklibMatch]; + } + } + + return typeof fn === "function" ? fn : null; +}; + const parseMethods = (methods) => { + if (!methods || typeof methods !== "string") { + console.error("[SplashKit WASM] Invalid method list received."); + return {}; + } + const methodList = methods .split(",") .map((method) => method.trim().replace("\n", "")) .filter(Boolean); const bindingsFunctions = {}; + const missingFunctions = []; + const errorFunctions = []; const frame = document.querySelector("iframe"); const wasmScope = frame?.contentWindow; for (const name of methodList) { try { - let fn = - globalThis[name] ?? - wasmScope?.[name] ?? - wasmScope?.[`_CPP_${name}`]; - - if (typeof fn !== "function") { - const sklibMatch = Object.keys(wasmScope || {}).find( - (key) => key === `__sklib__${name}` || key.startsWith(`__sklib__${name}__`) - ); - - if (sklibMatch && typeof wasmScope[sklibMatch] === "function") { - fn = wasmScope[sklibMatch]; - } - } + const fn = resolveBinding(name, wasmScope); - if (typeof fn === "function") { + if (fn) { bindingsFunctions[name] = fn; } else { - console.warn(`Missing binding: ${name}`); + missingFunctions.push(name); } } catch (e) { - console.warn(e); + errorFunctions.push(name); } } + // The current build is missing a large number of bindings, so log one + // summary line and hide the full list behind a collapsed group rather than + // printing a separate warning per name. + if (missingFunctions.length > 0) { + console.warn( + `[SplashKit WASM] ${missingFunctions.length} SplashKit bindings are missing.` + ); + + console.groupCollapsed("[SplashKit WASM] View missing bindings"); + missingFunctions.forEach((name) => console.warn(name)); + console.groupEnd(); + } + + if (errorFunctions.length > 0) { + console.warn( + `[SplashKit WASM] ${errorFunctions.length} SplashKit bindings could not be loaded.` + ); + + console.groupCollapsed("[SplashKit WASM] View binding loading errors"); + errorFunctions.forEach((name) => console.warn(name)); + console.groupEnd(); + } + return bindingsFunctions; }; @@ -50,6 +92,24 @@ const loadDotNet = async () => { const skFunctions = parseMethods(methods); + // process_events drives the event pump and the redraw, so C# programs stall + // without it. It is provided by the C++ side as __sko_process_events and is + // not always in scope when the bindings are first resolved, so it is looked + // up lazily on each call instead of being bound up front. + if (typeof skFunctions.process_events !== "function") { + const frame = document.querySelector("iframe"); + + skFunctions.process_events = () => { + const fn = resolveBinding("process_events", frame?.contentWindow); + + if (fn) { + return fn(); + } + + console.warn("[SplashKit WASM] process_events is not wired properly."); + }; + } + setModuleImports("main.js", { window: { location: { @@ -94,4 +154,4 @@ const CompileAndRun = async (code, reportError) => { // This event will be trigger by the csharp compiler document.addEventListener("compileAndRun", (ev) => { CompileAndRun(ev.detail.program[0].source, ev.detail.reportError); -}); \ No newline at end of file +});