diff --git a/CSharpWasmExpo/main.js b/CSharpWasmExpo/main.js index b8b414d6..05e84689 100644 --- a/CSharpWasmExpo/main.js +++ b/CSharpWasmExpo/main.js @@ -1,22 +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 { - bindingsFunctions[name] = eval(name); + const fn = resolveBinding(name, wasmScope); + + if (fn) { + bindingsFunctions[name] = fn; + } else { + 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; }; @@ -28,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: { @@ -62,6 +144,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;