Skip to content

Commit 90f0e34

Browse files
committed
fix(website): refresh IronRust WinForms examples
1 parent 85180ec commit 90f0e34

5 files changed

Lines changed: 95 additions & 62 deletions

File tree

content/docs/reference/ironrust/internals.md

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<!-- docs-title: Internals -->
22

3-
IronRust compiles RustScript bytecode into a CLR assembly and executes a generated `IPdVmProgram`. The WinForms path adds metadata-derived CLR bindings, object handles, a dedicated STA dispatcher, and an event queue.
3+
IronRust compiles RustScript bytecode into a CLR assembly and executes a generated `IPdVmProgram`. The WinForms path adds metadata-derived CLR bindings, object handles, callable RSS functions, and a main-form application session on the Runner's STA thread.
44

55
## Components
66

@@ -12,12 +12,14 @@ IronRust compiles RustScript bytecode into a CLR assembly and executes a generat
1212
| `PdVm.Compiler.PdVmVmbcReader` | Decodes VMBC into `PdVmProgramModel`. |
1313
| `PdVm.Compiler.PdVmStackAnalyzer` | Computes reachable operand-stack depths before IL emission. |
1414
| `PdVm.Compiler.PdVmClrCompiler` | Emits and saves the generated CLR program assembly. |
15-
| `PdVm.Runtime.PdVmProgramBase` | Stores resumable program state and implements shared host-call behavior. |
15+
| `PdVm.Runtime.PdVmProgramBase` | Stores resumable program state, real call frames, callable values, and the serialized callback queue. |
16+
| `PdVm.Runtime.IPdVmCallableProgram` | Exposes exported and runtime callable values, managed callback creation, reset, and shutdown. |
17+
| `PdVm.Runtime.PdVmScriptCallback` | Converts managed event arguments, posts RSS callable invocations, and optionally schedules them on a synchronization context. |
1618
| `PdVm.Runtime.PdVmAssemblyLoader` | Loads the generated assembly and creates its concrete `IPdVmProgram`. |
1719
| `PdVm.Runtime.PdVmExecution` | Drives `RunStep`, instruction budgets, yield, and asynchronous host completion. |
1820
| `PdVm.Runtime.PdVmDotNetHost` | Decodes exact CLR binding descriptors, converts values, and invokes CLR members. |
19-
| `PdVm.Runtime.PdVmWinFormsDispatcher` | Owns the WinForms STA thread and marshals UI work onto it. |
20-
| `PdVm.Runtime.PdVmWinFormsEventLoop` | Converts WinForms events into RustScript action strings and tracks form state. |
21+
| `PdVm.Runtime.PdVmWinFormsApplication` | Attaches a callable program to the current STA context, owns callback resources, registers the main form, and runs its message loop. |
22+
| `PdVm.Runtime.PdVmWinFormsEventLoop` | Binds WinForms events directly to RSS callable values and tracks close permission. |
2123

2224
## Source-to-assembly pipeline
2325

@@ -99,9 +101,11 @@ The operand stack normally lives in generated CLR locals. IronRust materializes
99101

100102
For a saved DLL, `PdVmAssemblyLoader.LoadProgram` registers the output directory for dependency resolution, loads the assembly, finds a non-abstract parameterless type implementing `IPdVmProgram`, and creates it.
101103

102-
For direct `.rss` execution, the Runner compiles to a temporary DLL and calls `PdVmAssemblyLoader.CreateProgram` on an in-memory assembly. Both paths then create the console host, register `PdVmDotNetHost.Call` as fallback host dispatch, and call `PdVmExecution.RunAsync`.
104+
For direct `.rss` execution, the Runner compiles to a temporary DLL and calls `PdVmAssemblyLoader.CreateProgram` on an in-memory assembly. Both paths then create the console host and register `PdVmDotNetHost.Call` as fallback host dispatch.
103105

104-
`PdVmExecution.RunAsync` repeatedly calls `RunStep` until the program halts. A waiting status is completed through `IAsyncPdVmHost.WaitAsync`, then passed back with `ResumePending`. `--max-steps` supplies the total instruction limit, while checks inside generated CLR code enforce the remaining budget during a single `RunStep` call.
106+
For non-WinForms profiles, `PdVmExecution.RunAsync` repeatedly calls `RunStep` until the program halts. A waiting status is completed through `IAsyncPdVmHost.WaitAsync`, then passed back with `ResumePending`. `--max-steps` supplies the total instruction limit, while checks inside generated CLR code enforce the remaining budget during a single `RunStep` call.
107+
108+
The WinForms profile requires a callable VMBC v10 program implementing `IPdVmCallableProgram`. The Runner sets a callback error observer, attaches `PdVmWinFormsApplication`, executes top-level RSS setup with `PdVmExecution.Run`, then calls `RunMessageLoop` when `Ui::Show` registered a main form.
105109

106110
## CLR values and object handles
107111

@@ -116,19 +120,12 @@ A generated release call removes both mappings. If the CLR object implements `ID
116120

117121
## WinForms threading and events
118122

119-
`PdVmWinFormsDispatcher` starts one background thread named `PdVm WinForms UI`, sets it to STA, initializes per-monitor-v2 DPI awareness and WinForms visual styles, creates a hidden `Form` as an invoker, and enters `Application.Run()`.
120-
121-
UI-bound exact calls are sent to that thread with `BeginInvoke`. `PdVmDotNetHost.RequiresWinFormsDispatcher` dispatches members from the `System.Windows.Forms` assembly and most `PdVmWinFormsEventLoop` methods there. `Wait` and `WaitTimeout` are deliberately excluded because they block while waiting for queued work.
122-
123-
`PdVmWinFormsEventLoop` stores one `FormState` per form in a `ConditionalWeakTable`. Each state contains:
123+
`Program.Main` carries `[STAThread]` and initializes WinForms before entering the Runner. `PdVmWinFormsApplication.Attach` records the callable program and host in an application session bound to that thread. `Ui::Show(form)` registers one main form; after top-level RSS setup completes, `RunMessageLoop` invokes `Application.Run(form)` on the same thread.
124124

125-
- `ConcurrentQueue<QueuedEvent>` for action strings and optional pointer snapshots
126-
- `AutoResetEvent` to wake `Wait` or `WaitTimeout`
127-
- the close-permission flag
128-
- the most recently dequeued pointer snapshot
125+
Every `BindClick`, `BindShown`, `BindClosing`, pointer, and timer binding accepts an RSS callable value. The application session converts that value into a `PdVmScriptCallback`, schedules it on a `ControlSynchronizationContext` backed by the form's `BeginInvoke`, and tracks both callback and event subscription for disposal.
129126

130-
A click or pointer event runs on the UI thread, enqueues a small record, and signals the waiter. The RustScript VM waits on its execution thread, so the WinForms message loop remains available.
127+
The CLR event handler calls `Post` and returns without running RustScript inside the managed event stack. `PdVmProgramBase` serializes callback work, restores the callable's function or closure environment, executes it through the generated program, and reports asynchronous failures through `CallbackErrorObserver`.
131128

132-
`BindClosing` installs a delegate matching the form's `FormClosing` event. Before an explicit close is allowed, the handler sets the event argument's `Cancel` property and queues the configured action. `Close` sets `AllowClose`, then invokes the form's `Close` method on the dispatcher thread.
129+
`BindClosing` installs a delegate matching the form's `FormClosing` event. Until an explicit close is allowed, the handler sets the event argument's `Cancel` property and posts the bound RSS callable. `Ui::Close` sets `AllowClose` and invokes the form's `Close` method from that callback.
133130

134-
This division keeps window creation and application behavior in `examples/dotnet-typed-winforms.rss`; the C# event adapter only provides thread marshalling, action delivery, dialog invocation, pointer snapshots, and close coordination.
131+
This division keeps window creation and application behavior in `examples/dotnet-typed-winforms.rss`; the C# layer supplies callable adaptation, UI-thread scheduling, dialog invocation, pointer payload conversion, timer subscriptions, and close coordination.

content/docs/reference/ironrust/winforms-example.md

Lines changed: 28 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ dotnet run --project PdVm.Runner -- run `
3030
--profile winforms
3131
```
3232

33-
The process remains active until the RustScript event loop handles the close action.
33+
The Runner executes the top-level RustScript setup, then keeps the process active with the main form's WinForms message loop until the form closes.
3434

3535
## Keep the compiled assembly
3636

@@ -81,7 +81,7 @@ CLR objects cross the RustScript boundary as integer handles. Strings, booleans,
8181

8282
Overloaded members receive generated suffixes such as `String`, `Bool`, or `Array`. Use the exact name accepted by the generated module; the example demonstrates names selected from the current .NET metadata, including `NewToolStripMenuItemString`.
8383

84-
## Event-loop methods
84+
## Callable event handlers
8585

8686
The example imports the IronRust adapter as `Ui`:
8787

@@ -91,38 +91,37 @@ use System::Windows::EventLoop as Ui;
9191

9292
| Method | What it does |
9393
| --- | --- |
94-
| `Ui::Show(form)` | Shows the form on IronRust's WinForms dispatcher thread. |
95-
| `Ui::BindClick(form, control, action)` | Subscribes to `Click` and queues the supplied action string. |
96-
| `Ui::BindDialog(form, control, dialog, action)` | Uses the control's click event to queue an action. The RustScript handler decides when to call `ShowDialog`. |
97-
| `Ui::BindClosing(form, action)` | Intercepts `FormClosing`, cancels that close attempt, and queues the action until RustScript calls `Ui::Close`. |
98-
| `Ui::Wait(form)` | Waits on the VM execution thread until an action is queued. The UI thread remains available to process Windows messages. |
99-
| `Ui::WaitTimeout(form, milliseconds)` | Returns a queued action, or an empty string after the timeout. |
94+
| `Ui::Show(form)` | Registers the main form. After top-level RSS setup halts, the Runner enters `Application.Run(form)` on its STA thread. |
95+
| `Ui::BindClick(form, control, callable)` | Subscribes to `Click` and posts a zero-argument RSS callable. |
96+
| `Ui::BindDialog(form, control, dialog, callable)` | Convenience click binding for a zero-argument RSS callable; the handler can call `ShowDialog`. |
97+
| `Ui::BindShown(form, callable)` | Posts a zero-argument RSS callable when the main form is shown. |
98+
| `Ui::BindClosing(form, callable)` | Cancels the close attempt and posts a zero-argument RSS callable until that handler calls `Ui::Close`. |
99+
| `Ui::BindMouseDown/Up/DoubleClick/Leave(form, control, callable)` | Posts an RSS callable with a map containing pointer-event data. |
100+
| `Ui::BindTimer(form, milliseconds, callable)` | Starts a WinForms timer and posts a zero-argument RSS callable on each tick. |
100101
| `Ui::ShowDialog(form, dialog)` | Calls `ShowDialog(IWin32Window)` with the form as owner and returns the dialog result name. |
101-
| `Ui::Close(form)` | Allows closing, then calls `Close` on the dispatcher thread. |
102-
| `Ui::BindPointer(form, control, prefix)` | Queues `<prefix>_down`, `_up`, `_double`, and `_leave` actions. |
103-
| `Ui::GetPointerButton/X/Y/Clicks(form)` | Reads the pointer snapshot attached to the most recently dequeued pointer event. |
102+
| `Ui::Close(form)` | Permits the pending close and closes the form. |
104103

105-
The application's behavior remains in RustScript:
104+
The checked-in Notepad example defines ordinary RSS functions, then binds closures directly to CLR events:
106105

107106
```rustscript
108-
Ui::BindDialog(form, open_item, open_dialog, "open");
109-
Ui::BindClick(form, wrap_item, "wrap");
110-
Ui::BindClosing(form, "close");
111-
Ui::Show(form);
112-
113-
while true {
114-
let action = Ui::Wait(form);
115-
if action == "open" {
116-
if Ui::ShowDialog(form, open_dialog) == "OK" {
117-
let path = OpenFileDialog::GetOpenFileDialogFileName(open_dialog);
118-
RichTextBox::SetRichTextBoxText(editor, File::ReadAllText(path));
119-
}
120-
}
121-
if action == "close" || action == "__closed" {
122-
Ui::Close(form);
123-
break;
107+
fn on_open() -> null {
108+
if Ui::ShowDialog(form, open_dialog) == "OK" {
109+
let current_path = OpenFileDialog::GetOpenFileDialogFileName(open_dialog);
110+
SaveFileDialog::SetSaveFileDialogFileName(save_dialog, current_path.copy());
111+
RichTextBox::SetRichTextBoxText(editor, File::ReadAllText(current_path.copy()));
124112
}
113+
null
125114
}
115+
116+
fn on_close() -> null {
117+
Ui::Close(form);
118+
null
119+
}
120+
121+
Ui::BindClick(form, open_item, || on_open());
122+
Ui::BindClick(form, wrap_item, || on_wrap());
123+
Ui::BindClosing(form, || on_close());
124+
Ui::Show(form);
126125
```
127126

128-
`BindDialog` does not display a dialog itself. It queues the action, then the RustScript branch opens the dialog and handles its result.
127+
The CLR event adapter returns immediately after posting the callback. IronRust serializes callbacks through the callable program and schedules each handler through the main form's synchronization context, so UI operations execute on the owning STA thread. The application no longer needs a polling loop: each click, close request, pointer event, or timer tick invokes its bound RSS handler directly.

index.html

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -315,18 +315,24 @@ <h2>Embedding references.</h2>
315315
repo: "https://github.com/rustscript-lang/IronRust",
316316
code: [
317317
'use System::Windows::Forms::Form;',
318+
'use System::Windows::Forms::Button;',
319+
'use System::Windows::Forms::Control::ControlCollection;',
318320
'use System::Windows::EventLoop as Ui;',
319321
'',
320322
'let form = Form::NewForm();',
321-
'Form::SetFormText(form, "RustScript on .NET");',
322-
'Form::SetFormWidth(form, 960);',
323-
'Form::SetFormHeight(form, 640);',
324-
'Form::SetFormStartPosition(form, "CenterScreen");',
323+
'let button = Button::NewButton();',
324+
'Button::SetButtonText(button, "Update title");',
325+
'ControlCollection::Add(Form::GetFormControls(form), button);',
325326
'',
326-
'Ui::UiBindClosing(form, "close");',
327-
'Ui::UiShow(form);',
328-
'Ui::UiWait(form);',
329-
'Ui::UiClose(form);',
327+
'fn on_click() -> null {',
328+
' Form::SetFormText(form, "Handled by RustScript");',
329+
' null',
330+
'}',
331+
'fn on_close() -> null { Ui::Close(form); null }',
332+
'',
333+
'Ui::BindClick(form, button, || on_click());',
334+
'Ui::BindClosing(form, || on_close());',
335+
'Ui::Show(form);',
330336
].join("\n"),
331337
},
332338
{

tests/carousel.test.mjs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,28 @@ const examples = [
8686
["blinky.rss", "Embedded", "https://github.com/rustscript-lang/micro-rustscript"],
8787
];
8888

89+
test("WinForms carousel uses direct RSS callable event handlers", () => {
90+
const page = createCarouselPage();
91+
const buttons = [...page.window.document.querySelectorAll(".example-dot")];
92+
93+
buttons[4].click();
94+
page.flushTimeouts();
95+
const code = page.window.document.getElementById("example-code").textContent;
96+
97+
for (const expected of [
98+
"fn on_close() -> null",
99+
"Ui::BindClosing(form, || on_close())",
100+
"Ui::Show(form)",
101+
]) {
102+
assert.ok(code.includes(expected), `WinForms carousel missing ${expected}`);
103+
}
104+
for (const retired of ["Ui::Ui", "Ui::Wait", "dispatcher"]) {
105+
assert.equal(code.includes(retired), false, `WinForms carousel still contains ${retired}`);
106+
}
107+
108+
page.window.close();
109+
});
110+
89111
test("carousel renders each example with its exact hardened GitHub action", () => {
90112
const page = createCarouselPage();
91113
const { document } = page.window;

tests/docs-build.test.mjs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -236,15 +236,19 @@ test("IronRust documents the checked-in WinForms example and its implementation
236236
"examples/dotnet-typed-winforms.rss",
237237
"--profile winforms",
238238
"System::Windows::EventLoop",
239-
"Ui::BindClick",
240-
"Ui::BindDialog",
241-
"Ui::BindClosing",
242-
"Ui::Wait",
239+
"fn on_open() -> null",
240+
"fn on_close() -> null",
241+
"Ui::BindClick(form, open_item, || on_open())",
242+
"Ui::BindClosing(form, || on_close())",
243+
"Ui::Show(form)",
243244
"Ui::ShowDialog",
244245
"Ui::Close",
245246
]) {
246247
assert.ok(exampleText.includes(expected), `WinForms guide missing ${expected}`);
247248
}
249+
for (const retired of ["PdVmWinFormsDispatcher", "Ui::Wait(", "Ui::WaitTimeout(", "action string"]) {
250+
assert.equal(exampleText.includes(retired), false, `WinForms guide still contains ${retired}`);
251+
}
248252
exampleWindow.close();
249253

250254
const internalsHtml = await readFile(new URL("../public/docs/reference/ironrust/internals/index.html", import.meta.url), "utf8");
@@ -261,13 +265,18 @@ test("IronRust documents the checked-in WinForms example and its implementation
261265
"PdVmAssemblyLoader",
262266
"PdVmExecution",
263267
"PdVmDotNetHost",
264-
"PdVmWinFormsDispatcher",
268+
"IPdVmCallableProgram",
269+
"PdVmScriptCallback",
270+
"PdVmWinFormsApplication",
265271
"PdVmWinFormsEventLoop",
266-
"ConcurrentQueue",
267-
"AutoResetEvent",
272+
"ControlSynchronizationContext",
273+
"RunMessageLoop",
268274
]) {
269275
assert.ok(internalsText.includes(expected), `IronRust internals missing ${expected}`);
270276
}
277+
for (const retired of ["PdVmWinFormsDispatcher", "Ui::Wait", "Ui::WaitTimeout", "AutoResetEvent"]) {
278+
assert.equal(internalsText.includes(retired), false, `IronRust internals still contains ${retired}`);
279+
}
271280

272281
const ironRustLink = [...window.document.querySelectorAll(".docs-nav-item a")]
273282
.find((link) => link.textContent === "IronRust");

0 commit comments

Comments
 (0)