Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 88 additions & 2 deletions CSharpWasmExpo/main.js
Original file line number Diff line number Diff line change
@@ -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;
};

Expand All @@ -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: {
Expand Down Expand Up @@ -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"));
}
};

Expand Down
46 changes: 45 additions & 1 deletion runtimes/csharp/csharpRuntime.js
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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;
Expand Down
Loading