maruzzella 0.1.0

GTK4 desktop shell prototype in Rust with persisted layouts and plugin-backed views.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
# Plugin ABI RFC v1

## Goal

Maruzzella should support dynamic plugins from the beginning without giving up:

- rich Rust implementations inside plugins
- custom GTK widgets created by plugins
- plugin dependency resolution
- stable host/plugin loading semantics

At the same time, Maruzzella must not rely on the unstabilized Rust ABI across dynamic library boundaries.

This RFC defines the first plugin boundary for Maruzzella.

## Non-Goals

This RFC does not define:

- final end-user plugin packaging or installation UX
- a networked or sandboxed plugin runtime
- plugin unloading
- hot reloading
- a finalized configuration schema
- every possible shell contribution surface

## Core Principles

1. Plugins are native dynamic libraries loaded at runtime.
2. Plugin internals may use full Rust and gtk-rs.
3. The host/plugin boundary must be ABI-safe.
4. Plugins do not arbitrarily mutate shell UI.
5. Plugins contribute structure and behavior through host-owned registration APIs.
6. GTK objects may cross the boundary only through GObject/GTK-compatible pointer forms.
7. Active plugins are never unloaded during process lifetime.

## Layering

The plugin architecture is split into four layers:

- `maruzzella`
  The host application, plugin loader, registry, runtime, and shell renderer.
- `maruzzella_api`
  ABI-safe types, C-compatible entrypoints, host callback tables, plugin descriptors, and shared contribution contracts.
- `maruzzella_sdk`
  Ergonomic Rust wrappers for plugin authors that hide most ABI details.
- plugin crates
  Independent dynamic libraries compiled outside Maruzzella and loaded at runtime.

The SDK is the ergonomic layer. The API crate is the binary contract.

## Startup Model

At startup, Maruzzella should:

1. Build a `ProductConfig`.
2. Load the built-in base plugin.
3. Discover user-provided plugin libraries.
4. Resolve plugin descriptors and dependency graph.
5. Reject or disable plugins with unsatisfied hard dependencies.
6. Register contributions from resolved plugins.
7. Build menus, actions, settings surfaces, and shell views from merged contributions.
8. Create plugin-provided widgets on demand.

## ProductConfig Scope

`ProductConfig` should remain intentionally small. It should contain:

- application id
- persistence namespace
- branding
- basic shell defaults
- plugin search paths or plugin manifests

It should not become the place where downstream products directly assemble full shell behavior.

## Plugin Model

Each plugin has:

- an id
- a semantic version
- an API compatibility version
- a dependency list
- optional contribution declarations
- optional runtime hooks

Each plugin should be uniquely identified by a stable string id, for example:

- `maruzzella.base`
- `com.example.notes`
- `org.example.git`

## Exported Entry Point

Each plugin dynamic library exports one known symbol:

```rust
extern "C" fn maruzzella_plugin_entry() -> *const MzPluginVTable
```

The vtable is the root ABI object for the plugin.

## Plugin VTable

The exact naming may change, but v1 should contain at least:

- `abi_version`
- `descriptor`
- `register`
- `startup`
- `shutdown`

Conceptually:

```rust
#[repr(C)]
pub struct MzPluginVTable {
    pub abi_version: u32,
    pub descriptor: extern "C" fn() -> MzPluginDescriptorView,
    pub register: extern "C" fn(host: *const MzHostApi) -> MzStatus,
    pub startup: extern "C" fn(host: *const MzHostApi) -> MzStatus,
    pub shutdown: extern "C" fn(host: *const MzHostApi),
}
```

`register` is for declaring contributions and handlers.

`startup` is for imperative initialization after registration succeeds.

`shutdown` is best-effort cleanup only. It must not imply plugin unloading.

## Descriptor

The plugin descriptor must include:

- plugin id
- human-readable name
- semantic version
- required Maruzzella ABI version
- optional description
- dependency list

Conceptually:

```rust
#[repr(C)]
pub struct MzPluginDescriptorView {
    pub id: MzStr,
    pub name: MzStr,
    pub version: MzVersion,
    pub required_abi_version: u32,
    pub description: MzStr,
    pub dependencies_ptr: *const MzPluginDependency,
    pub dependencies_len: usize,
}
```

## Dependencies

Plugin dependencies are runtime dependencies, not automatic Rust code dependencies.

Each dependency should declare:

- target plugin id
- minimum version
- maximum version or compatible major range later
- required vs optional

Conceptually:

```rust
#[repr(C)]
pub struct MzPluginDependency {
    pub plugin_id: MzStr,
    pub min_version: MzVersion,
    pub max_version_exclusive: MzVersion,
    pub required: bool,
}
```

Resolution rules for v1:

- if a required dependency is missing, the plugin is not activated
- if a required dependency version is incompatible, the plugin is not activated
- optional dependencies may be ignored for v1
- dependency cycles are an error

## ABI Rules

Across the dynamic library boundary, Maruzzella must not expose ordinary Rust ABI types as the contract.

Forbidden as boundary types:

- `String`
- `&str`
- `Vec<T>`
- plain Rust trait objects
- `Box<T>` with cross-boundary ownership
- normal Rust closures
- ordinary Rust enums without explicit ABI design
- GTK Rust wrapper objects like `gtk::Widget`

Allowed boundary forms:

- primitive integers and booleans
- `#[repr(C)]` structs
- explicitly ABI-safe tagged enums
- pointer + length views
- opaque handles
- `extern "C"` function pointers
- UTF-8 string views
- byte slices
- GObject or GTK instance pointers

## Shared ABI Helper Types

The API crate should define a small set of shared ABI-safe helpers:

- `MzStr`
  UTF-8 bytes as pointer + length
- `MzBytes`
  opaque byte payload
- `MzVersion`
  semantic version components
- `MzStatus`
  success/error status code
- `MzHandle`
  opaque numeric or pointer handle

Example:

```rust
#[repr(C)]
pub struct MzStr {
    pub ptr: *const u8,
    pub len: usize,
}
```

The SDK should wrap these into ergonomic Rust types on the plugin side.

## Host API

The host exposes a callback table to plugins during registration and runtime.

The host API should cover only controlled operations. It should not expose arbitrary shell internals.

The host must provide registration functions for:

- commands
- menu items
- toolbar items
- settings pages
- dialogs or modal entries
- contribution surfaces
- view factories

The host API should also provide runtime services for:

- logging
- querying plugin metadata
- config read/write
- command dispatch
- looking up registered surfaces
- optional service discovery later

Conceptually:

```rust
#[repr(C)]
pub struct MzHostApi {
    pub abi_version: u32,
    pub log: extern "C" fn(level: u32, message: MzStr),
    pub register_command: extern "C" fn(command: *const MzCommandSpec) -> MzStatus,
    pub register_menu_item: extern "C" fn(item: *const MzMenuItemSpec) -> MzStatus,
    pub register_surface_contribution: extern "C" fn(contribution: *const MzSurfaceContribution) -> MzStatus,
    pub register_view_factory: extern "C" fn(factory: *const MzViewFactorySpec) -> MzStatus,
    pub dispatch_command: extern "C" fn(command_id: MzStr, payload: MzBytes) -> MzStatus,
}
```

The final host API will likely be split into separate registration and runtime tables, but this is enough for v1 design.

## Declarative Contributions

Plugins must be able to contribute declarative structure to the shell, including:

- commands
- menus
- toolbar items
- settings pages
- dialogs
- view descriptors
- shell surfaces
- contributions to existing surfaces

Examples of host-owned surfaces:

- `maruzzella.menu.file.items`
- `maruzzella.menu.help.items`
- `maruzzella.about.sections`
- `maruzzella.plugins.settings_pages`

The base plugin should define the first shared shell-level contribution surfaces.

## Imperative Behavior

Plugins must also support arbitrary code execution through controlled hooks.

V1 hooks should include:

- registration
- startup
- shutdown
- command invocation
- widget creation

This allows plugins to:

- initialize state
- spawn async work later
- handle actions
- create dynamic UI
- react to shell requests

Imperative behavior must always flow through host-defined entrypoints and contracts.

## Commands

Commands are globally identified by string ids.

A plugin may register:

- command metadata
- a command handler entrypoint

The host owns command dispatch. Plugins do not directly wire menu callbacks into shell widgets.

Command payloads for v1 should be byte payloads or empty payloads. The SDK may offer typed serialization helpers on top.

## Menus

The root menu should begin empty at the core host level.

Menus should be assembled from plugin contributions.

The built-in base plugin contributes at least:

- a `File` menu
- a `Plugins` entry under `File`
- a `Help` menu or equivalent
- an `About` entry

Menu items should reference command ids, not direct callbacks.

## Base Plugin

`maruzzella.base` is the first real plugin and must be loaded by default.

Its responsibilities:

- register core shell commands
- define root shell contribution surfaces
- provide the plugins management modal
- provide the about modal
- contribute base menu items
- host plugin configuration UI surfaces

The base plugin should not hardcode downstream product metadata. It should read product branding from host-provided product context.

Other plugins should be able to depend on `maruzzella.base` and contribute, for example, additional about sections.

## GTK Widget Factories

Plugins must be able to create custom GTK widgets.

This is allowed because GTK and GObject already operate on a stable C ABI.

The important rule is that the ABI boundary uses GObject/GTK-compatible pointers, not ordinary Rust wrapper types.

For example, a plugin may register a view factory identified by a string id. When the host needs that view, it calls into the plugin and receives a widget pointer.

Conceptually:

```rust
#[repr(C)]
pub struct MzViewFactorySpec {
    pub plugin_id: MzStr,
    pub view_id: MzStr,
    pub create: extern "C" fn(host: *const MzHostApi, request: *const MzViewRequest) -> *mut gtk_sys::GtkWidget,
}
```

The final API crate should not depend directly on a high-level gtk-rs crate for this ABI. It should use raw GTK/GObject pointer forms or compatible wrappers.

## Widget Ownership And Lifetime

V1 ownership rules:

- the plugin creates the widget instance
- the plugin transfers ownership to the host as a GTK object reference
- the host becomes responsible for normal GTK lifetime management after adoption
- the plugin library must remain loaded for the life of any object whose implementation lives in that library
- active plugins are therefore never unloaded

Any plugin that registers GTypes, subclasses GTK widgets, installs signals, or returns widget instances must be considered permanently resident.

## Structured Contribution Contracts

Some contribution surfaces need structured payloads, such as:

- about sections
- settings pages
- diagnostics providers

These contracts must not be invented ad hoc inside a plugin if other plugins are expected to consume them.

They must live in a shared contract location:

- `maruzzella_api` for generic shell-level contracts
- dedicated shared API crates for domain-specific ecosystems

This allows:

- compile-time agreement on the contract
- runtime dependency on the provider plugin

Example:

- `maruzzella.base` hosts `maruzzella.about.sections`
- the shape of an about section is defined in `maruzzella_api`
- another plugin depends at runtime on `maruzzella.base`
- that plugin contributes an about section using the shared contract type

If a structured contract becomes too unstable or too rich for the v1 ABI, it should cross the boundary as serialized bytes and be decoded by the host-side SDK layer.

## Error Handling

Plugin-facing ABI calls should return explicit status codes, not panic across the boundary.

Rules:

- panics must not unwind across the host/plugin ABI boundary
- plugin-side SDK should catch panics and translate them into error statuses where possible
- malformed contributions are rejected by the host with diagnostics
- incompatible ABI version prevents plugin activation

## Discovery And Packaging

V1 discovery may be simple:

- explicit plugin library paths in config
- a plugin directory scanned at startup

Packaging format can remain unspecified for now. The key requirement is that the host receives a filesystem path to a dynamic library compatible with the current platform.

## Platform Artifacts

Plugin libraries will be platform-native artifacts, for example:

- `.so` on Linux
- `.dylib` on macOS
- `.dll` on Windows

The API and SDK should avoid platform-specific assumptions outside library loading details.

## Security

V1 plugins are trusted native code.

This implies:

- full process access
- no isolation
- no sandboxing

That is acceptable for v1, but it must be stated clearly in documentation.

## Implementation Order

Recommended implementation order:

1. Create `maruzzella_api` with ABI-safe primitives and plugin descriptor types.
2. Create `maruzzella_sdk` with safe wrappers around descriptor/export boilerplate.
3. Add loader support in `maruzzella` using `libloading`.
4. Implement plugin descriptor loading and dependency resolution.
5. Implement `maruzzella.base` as the first plugin.
6. Implement command and menu contribution registration.
7. Implement widget factory registration.
8. Add one external sample plugin outside the core crate tree.

## Open Questions

- Should structured payloads default to ABI-safe structs or serialized bytes?
- Should view factories be synchronous only in v1?
- How much of plugin configuration belongs in the base plugin vs host core?
- Do we want plugin enable/disable state persisted by plugin id and version?
- Do we need plugin capability flags in addition to dependencies?
- Should the host expose a generic service registry in v1 or later?

## Recommended Decision

Proceed with dynamic plugins from day one, but only with:

- a strict ABI-safe boundary
- a dedicated API crate
- an SDK for ergonomic Rust authoring
- a built-in base plugin that proves the contribution model
- no plugin unloading

This preserves the original product direction while avoiding the fragility of pretending that ordinary Rust dylib boundaries are stable enough for a plugin platform.