kui-ffi 0.1.0-alpha.52

C API for kui (cdylib, staticlib on request, + include/kui.h)
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
513
514
515
516
517
518
//! Slots across the C boundary.
//!
//! A C **host** declares a slot with `kui_slot` the way a Rust host calls
//! `Ui::slot`, and loads what fills it with `kui_ctx_add_extension`. A C
//! **extension** reads which slot it is filling with
//! `kui_slot_name` / `kui_slot_params`, and answers an event with
//! `kui_reply`, whose sink rides on the event (ABI 10) so that a plugin
//! linked against another copy of this library still reaches the host.

use super::*;

/// Declares a slot named `name` at the cursor, with `params` (may be NULL)
/// for whatever fills it, and fills it then and there with the extension
/// the name's namespace belongs to. `name` is the full `namespace/slot`:
/// the namespace this context loaded the extension under with
/// [`kui_ctx_add_extension`], and the slot in the extension's own
/// vocabulary.
///
/// Returns false when the frame already declared this name (a duplicate,
/// which warns) or on a bad context; true when the slot was declared,
/// whether or not anything filled it. `kui_key_of` answers its key either
/// way, so a host with no extensions loaded still gets a placed, empty
/// node — which is what this did for every host before extensions could be
/// loaded from C at all.
///
/// A C extension may call this from its own view too, and the slot is
/// declared inside its fill and keyed there. What it cannot do is load
/// the thing that fills it: a plugin's context has no list of its own, so
/// the name it declares has to be one the host above it already loaded
/// (`kui_core::slot`, and `env.add_extension` in Lua for the side that
/// can load). Its own slot is the one name that finds nobody, since it is
/// out of the list while it draws — that warns and draws nothing.
#[unsafe(no_mangle)]
pub extern "C" fn kui_slot(ptr: *mut KuiCtx, name: KuiStr, params: *const KuiValue) -> bool {
    guard(false, || {
        let Some(c) = (unsafe { ctx(ptr) }) else {
            return false;
        };
        let name = kstr(name);
        let params = unsafe { params.as_ref() }.map_or(&kui_core::Value::Null, |v| &v.0);
        // `Ui::slot_with` is `begin_slot` and the fill together, so the
        // one question is which `Ui`: under `kui_run_with` the runner's
        // own, which carries its extension list and which `borrowing_in`
        // kept a pointer to; otherwise one made here around the context's
        // core and *its* list. The core is behind a raw pointer on the
        // context so that both can be borrowed at once (`with_host_ui`).
        with_host_ui(c, |ui| ui.slot_with(&name, params))
    })
}

/// The `Ui` a host's slot call goes through: under `kui_run_with` the
/// runner's own, which carries its extension list; otherwise one made
/// around the context's core and *its* list. See [`kui_slot`].
fn with_host_ui<T>(c: &mut KuiCtx, f: impl FnOnce(&mut kui_core::Ui<'_>) -> T) -> T {
    if c.host_ui.is_null() {
        let mut local = kui_core::Ui::with_filler(unsafe { &mut *c.core }, &mut c.extensions);
        f(&mut local)
    } else {
        f(unsafe { &mut *c.host_ui.cast() })
    }
}

/// [`kui_slot`], and what the fill built is kept for [`kui_slot_replay`]
/// to push again next frame (ADR 0045): every node as its door saw it,
/// the slots declared inside, every fact of the frame the fill read.
/// Returns what `kui_slot` returns.
#[unsafe(no_mangle)]
pub extern "C" fn kui_slot_kept(ptr: *mut KuiCtx, name: KuiStr, params: *const KuiValue) -> bool {
    guard(false, || {
        let Some(c) = (unsafe { ctx(ptr) }) else {
            return false;
        };
        let name = kstr(name);
        let params = unsafe { params.as_ref() }.map_or(&kui_core::Value::Null, |v| &v.0);
        with_host_ui(c, |ui| ui.slot_kept(&name, params))
    })
}

/// The host's claim that nothing it feeds the extension filling `name`
/// has changed since the fill was kept (ADR 0045). The core checks what
/// it can see — the params, every fact of the frame the kept fill read,
/// that the slot is declared where it was — and pushes the kept nodes
/// again without asking the extension (`KUI_SLOT_REPLAYED`), or fills
/// and keeps it as `kui_slot_kept` would and says why (the other
/// `KUI_SLOT_*` codes). `KUI_SLOT_UNDECLARED` when the slot was not
/// declared: a duplicate, or outside a frame.
#[unsafe(no_mangle)]
pub extern "C" fn kui_slot_replay(ptr: *mut KuiCtx, name: KuiStr, params: *const KuiValue) -> i32 {
    guard(-1, || {
        let Some(c) = (unsafe { ctx(ptr) }) else {
            return -1;
        };
        let name = kstr(name);
        let params = unsafe { params.as_ref() }.map_or(&kui_core::Value::Null, |v| &v.0);
        with_host_ui(c, |ui| ui.slot_replay(&name, params)).map_or(-1, |f| f.code())
    })
}

/// What the last `kui_slot_replay` of `name` answered this frame (or the
/// frame before, while this one is being built), as its `KUI_SLOT_*`
/// code; `KUI_SLOT_UNDECLARED` when it was not asked — a frame that kept
/// the slot with [`kui_slot_kept`], filled it plainly or skipped it.
#[unsafe(no_mangle)]
pub extern "C" fn kui_slot_fill(ptr: *mut KuiCtx, name: KuiStr) -> i32 {
    guard(-1, || {
        let Some(c) = (unsafe { ctx(ptr) }) else {
            return -1;
        };
        let name = kstr(name);
        unsafe { &*c.core }
            .slot_fill(&name)
            .map_or(-1, |f| f.code())
    })
}

/// Which slot this context is a fill of; false (and `out` untouched) on
/// a context that is not an extension's — a standalone one, or a C host's
/// view callback. Borrowed for the duration of `kui_ext_view`.
#[unsafe(no_mangle)]
pub extern "C" fn kui_slot_name(ptr: *mut KuiCtx, out: *mut KuiStr) -> bool {
    guard(false, || {
        let Some(c) = (unsafe { ctx(ptr) }) else {
            return false;
        };
        let (Some(name), Some(out)) = (c.slot_name.as_deref(), unsafe { out.as_mut() }) else {
            return false;
        };
        *out = KuiStr {
            ptr: name.as_ptr(),
            len: name.len(),
        };
        true
    })
}

/// The namespace the host loaded this extension under — what makes the
/// slot's full name, and what tells one instance of a plugin loaded twice
/// from the other. False (and `out` untouched) on a context that is not
/// an extension's. Borrowed for the duration of `kui_ext_view`.
#[unsafe(no_mangle)]
pub extern "C" fn kui_slot_namespace(ptr: *mut KuiCtx, out: *mut KuiStr) -> bool {
    guard(false, || {
        let Some(c) = (unsafe { ctx(ptr) }) else {
            return false;
        };
        let (Some(ns), Some(out)) = (c.slot_namespace.as_deref(), unsafe { out.as_mut() }) else {
            return false;
        };
        *out = KuiStr {
            ptr: ns.as_ptr(),
            len: ns.len(),
        };
        true
    })
}

/// The parameters the host declared the slot with, or NULL when it passed
/// none (or this is not an extension's context). Borrowed for the
/// duration of `kui_ext_view`; read it with `kui_value_get` and friends.
#[unsafe(no_mangle)]
pub extern "C" fn kui_slot_params(ptr: *mut KuiCtx) -> *const KuiValue {
    guard(std::ptr::null(), || {
        let Some(c) = (unsafe { ctx(ptr) }) else {
            return std::ptr::null();
        };
        c.slot_params
            .as_ref()
            .map_or(std::ptr::null(), |v| v as *const KuiValue)
    })
}

/// The host's end of a reply sink: the [`KuiReplySink`] header a plugin is
/// handed, and the list only this copy of the library ever touches.
///
/// `repr(C)` with `head` first, so the `*mut KuiReplySink` on the event can
/// be cast back to this. Nothing outside this file knows the second field
/// exists — least of all the plugin, which has only the header's function
/// pointer and calls through it.
#[repr(C)]
struct HostSink {
    head: KuiReplySink,
    replies: Vec<Value>,
}

/// The `push` a plugin's `kui_reply` forwards to. Runs in the copy of the
/// library that opened the sink, which is the whole reason it is a function
/// pointer: see [`KuiReplySink`].
unsafe extern "C" fn push_reply(sink: *mut KuiReplySink, value: *const KuiValue) -> bool {
    guard(false, || {
        let (Some(sink), Some(value)) = (unsafe { sink.cast::<HostSink>().as_mut() }, unsafe {
            value.as_ref()
        }) else {
            return false;
        };
        sink.replies.push(value.0.clone());
        true
    })
}

/// Runs `cb` (a plugin's `kui_ext_on_event`) with a reply sink open on
/// `ev`, and returns what it replied. The sink lives on this stack frame
/// for exactly the call: `ev.reply_sink` points at it, and `push` is
/// cleared before it goes, so a plugin that stored the event and calls
/// afterwards is refused rather than heard.
///
/// Nested calls cannot happen — the runner delivers events one at a time —
/// but nothing here would mind if they did, which is the other thing the
/// process-global this replaced could not say.
pub(crate) fn collect_replies(ev: &mut KuiEvent, cb: impl FnOnce(&KuiEvent)) -> Vec<Value> {
    let mut sink = HostSink {
        head: KuiReplySink {
            push: Some(push_reply),
        },
        replies: Vec::new(),
    };
    ev.reply_sink = (&raw mut sink).cast::<KuiReplySink>();
    cb(ev);
    ev.reply_sink = std::ptr::null_mut();
    sink.head.push = None;
    sink.replies
}

/// Replies to the host from inside `kui_ext_on_event`: `ev` is the event
/// the callback was handed, `reply` is copied (you keep ownership) and
/// reaches the host's `on_event` with your origin and the event's window
/// and key. Call it as often as the event deserves. Outside the callback,
/// or on an event that carries no sink (anything a host polled for
/// itself), it does nothing and returns false.
///
/// It forwards through the sink on the event rather than into a list of
/// its own, so it works when the plugin's copy of this library is not the
/// host's ([`KuiReplySink`]).
#[unsafe(no_mangle)]
pub extern "C" fn kui_reply(ev: *const KuiEvent, reply: *const KuiValue) -> bool {
    guard(false, || {
        let (Some(ev), Some(reply)) = (unsafe { ev.as_ref() }, unsafe { reply.as_ref() }) else {
            return false;
        };
        let sink = ev.reply_sink;
        let Some(push) = (unsafe { sink.as_ref() }).and_then(|s| s.push) else {
            return false;
        };
        unsafe { push(sink, reply) }
    })
}

// ---------------------------------------------------------------------------
// Loading extensions from C

/// Loads the shared library at `path` as an extension of this context,
/// under `namespace` — the word that makes the front of every slot name it
/// fills (`namespace/panel`). An empty `namespace` takes the extension's
/// own `kui_ext_name`, which is what a Rust host's `Extensions::push` does.
///
/// False on any refusal, with the reason readable until the next call
/// through [`kui_ctx_extension_error`]: the library will not load, it
/// declares no `kui_ext_abi` or one this build does not implement, it has
/// no `kui_ext_view`, the namespace is empty *and* the plugin named
/// itself nothing, or the namespace is already another extension's.
///
/// The context owns the extension from here: it is unloaded by
/// `kui_ctx_free`, after the plugin's own `kui_ext_free`. Load before the
/// first frame — origins are positions in this list, so a plugin added
/// between frames renumbers the ones after it.
///
/// It has to be a context of your own, so this refuses one that borrows
/// somebody else's frame — a plugin's `kui_ext_view`, or a view callback
/// under `kui_run_with`. Such a context fills its slots from the list one
/// level up (`host_ui`) and dies at the end of the call, so a plugin
/// loaded into it would be unloaded again having drawn nothing. Saying so
/// is the point: the alternative is that this answers true and the
/// `kui_slot` after it quietly draws an empty node.
///
/// # Safety
/// The library's entry points run in this process on this thread, on the
/// frame this context owns. Loading one is trusting it exactly as much as
/// linking it would be.
#[unsafe(no_mangle)]
pub extern "C" fn kui_ctx_add_extension(ptr: *mut KuiCtx, namespace: KuiStr, path: KuiStr) -> bool {
    guard(false, || {
        let Some(c) = (unsafe { ctx(ptr) }) else {
            return false;
        };
        c.last_ext_error.clear();
        if !c.host_ui.is_null() {
            // A borrowed frame: this context's list is not the one filling
            // its slots, so anything put in it would be loaded, never
            // asked to draw, and unloaded when the call returns.
            c.last_ext_error = "cannot load an extension into a context that borrows a frame: \
                                its slots are filled from the list one level up. Load into a \
                                context of your own, before `kui_run_with`"
                .to_owned();
            return false;
        }
        // SAFETY: the caller's, and the doc comment says so. An empty
        // namespace is the extension's own name — `push_as`'s rule, not
        // one repeated here.
        let loaded = unsafe { crate::CExtension::open(&*kstr(path)) }
            .and_then(|ext| c.extensions.push_as(kstr(namespace), Box::new(ext)));
        if let Err(err) = loaded {
            c.last_ext_error = err;
        }
        c.last_ext_error.is_empty()
    })
}

/// Why the last [`kui_ctx_add_extension`] on this context said false.
/// False (and `out` untouched) when the last one succeeded, or when none
/// has run. Borrowed until the next call on this context, like every other
/// string this API hands back.
#[unsafe(no_mangle)]
pub extern "C" fn kui_ctx_extension_error(ptr: *mut KuiCtx, out: *mut KuiStr) -> bool {
    guard(false, || {
        let Some(c) = (unsafe { ctx(ptr) }) else {
            return false;
        };
        let Some(out) = (unsafe { out.as_mut() }) else {
            return false;
        };
        if c.last_ext_error.is_empty() {
            return false;
        }
        *out = KuiStr {
            ptr: c.last_ext_error.as_ptr(),
            len: c.last_ext_error.len(),
        };
        true
    })
}

/// How many extensions this context has loaded. Their origins are 1..=n,
/// in the order they were added; 0 is the host's own.
#[unsafe(no_mangle)]
pub extern "C" fn kui_ctx_extension_count(ptr: *mut KuiCtx) -> u32 {
    guard(0, || {
        unsafe { ctx(ptr) }.map_or(0, |c| c.extensions.len() as u32)
    })
}

/// The namespace the extension at `origin` was loaded under — what turns
/// an event's `origin` back into a name the host chose. False (and `out`
/// untouched) for 0 (the host) or an origin nothing was loaded at.
/// Borrowed until the next call.
#[unsafe(no_mangle)]
pub extern "C" fn kui_ctx_extension_namespace(
    ptr: *mut KuiCtx,
    origin: u16,
    out: *mut KuiStr,
) -> bool {
    guard(false, || {
        let (Some(c), Some(out)) = (unsafe { ctx(ptr) }, unsafe { out.as_mut() }) else {
            return false;
        };
        let Some(ns) = c.extensions.namespace_of(kui_core::OriginId(origin)) else {
            return false;
        };
        *out = KuiStr {
            ptr: ns.as_ptr(),
            len: ns.len(),
        };
        true
    })
}
#[cfg(test)]
mod tests {
    use super::*;

    fn ks(s: &str) -> KuiStr {
        KuiStr {
            ptr: s.as_ptr(),
            len: s.len(),
        }
    }

    /// A C host's slot: declared at the cursor, findable by name, and a
    /// second declaration of the name refused with the warning.
    #[test]
    fn a_host_slot_is_declared_and_named() {
        let ctx = kui_ctx_new();
        // A standalone context starts with diagnostics off; the duplicate
        // below is what they are for here.
        kui_set_diagnostics(ctx, true);
        kui_frame_begin(ctx, 200.0, 100.0, 1.0);
        let root: KuiSpec = unsafe { std::mem::zeroed() };
        kui_root(ctx, &root);
        assert!(kui_slot(ctx, ks("side"), std::ptr::null()));
        assert!(
            !kui_slot(ctx, ks("side"), std::ptr::null()),
            "declared twice"
        );
        kui_frame_finish(ctx);
        assert_eq!(
            kui_key_of(ctx, ks("side")),
            kui_core::Key::ROOT.str("side").0,
            "the slot's key is `parent.str(name)`, and `kui_key_of` answers it"
        );
        let c = unsafe { ctx.as_mut() }.unwrap();
        let ws = c.core().take_warnings();
        assert_eq!(ws.len(), 1);
        assert_eq!(ws[0].code, kui_core::diag::DUPLICATE_SLOT);
        // Not an extension's context: no slot to read.
        let mut name = ks("");
        assert!(!kui_slot_name(ctx, &mut name));
        assert!(!kui_slot_namespace(ctx, &mut name));
        assert!(kui_slot_params(ctx).is_null());
        kui_ctx_free(ctx);
    }

    /// `kui_reply` reaches the host only through the sink the callback was
    /// handed, and only while it is open.
    #[test]
    fn replies_are_collected_for_the_event_in_progress_only() {
        let payload = KuiValue(Value::Null);
        let mut ev = KuiEvent {
            payload: &payload,
            ..Default::default()
        };
        // An event nobody opened a sink on: what a host polls for itself.
        let other = KuiEvent {
            payload: &payload,
            ..Default::default()
        };
        let reply = KuiValue(Value::map([("kind", "open".into())]));
        assert!(!kui_reply(&ev, &reply), "no callback in progress");
        let got = collect_replies(&mut ev, |ev| {
            assert!(kui_reply(ev, &reply));
            assert!(!kui_reply(&other, &reply), "an event with no sink");
            assert!(!kui_reply(ev, std::ptr::null()), "no value");
            assert!(kui_reply(ev, &reply));
        });
        assert_eq!(got, vec![reply.0.clone(), reply.0.clone()]);
        assert!(!kui_reply(&ev, &reply), "the sink closed with the callback");
        assert!(ev.reply_sink.is_null(), "and the event no longer names one");
    }

    /// The C loader's refusals, and what it says about them. The happy
    /// path needs a real plugin and so lives in `examples/c/features/slots/host.c`'s
    /// `--headless`, which the build scripts run; this is the half that
    /// needs no compiler.
    #[test]
    fn a_c_host_is_told_why_an_extension_would_not_load() {
        let ctx = kui_ctx_new();
        assert_eq!(kui_ctx_extension_count(ctx), 0);

        let mut out = ks("");
        assert!(
            !kui_ctx_extension_error(ctx, &mut out),
            "nothing has been tried yet"
        );

        // The platform's own C runtime: loads, and declares no kui_ext_abi —
        // which is the plugin built against a header from before the symbol
        // existed, the case the check is for.
        let lib = crate::ext::tests::a_library_with_no_kui_symbols();
        assert!(!kui_ctx_add_extension(ctx, ks("libc"), ks(lib)));
        assert!(kui_ctx_extension_error(ctx, &mut out));
        let msg =
            std::str::from_utf8(unsafe { std::slice::from_raw_parts(out.ptr, out.len) }).unwrap();
        assert!(
            msg.contains("declares no ABI"),
            "the reason should be the ABI refusal, got {msg:?}"
        );
        assert_eq!(kui_ctx_extension_count(ctx), 0, "nothing was kept");

        // A path that is not a library at all.
        assert!(!kui_ctx_add_extension(
            ctx,
            ks("nope"),
            ks("no/such/library")
        ));
        assert!(kui_ctx_extension_error(ctx, &mut out));

        // No extension at any origin, so nothing names one.
        assert!(
            !kui_ctx_extension_namespace(ctx, 0, &mut out),
            "0 is the host"
        );
        assert!(!kui_ctx_extension_namespace(ctx, 1, &mut out));

        // And a slot still places its node with nothing to fill it, which is
        // what lets a host lay out before it has a plugin.
        kui_frame_begin(ctx, 200.0, 100.0, 1.0);
        assert!(kui_slot(ctx, ks("todos/panel"), std::ptr::null()));
        assert!(
            !kui_slot(ctx, ks("todos/panel"), std::ptr::null()),
            "duplicate"
        );
        kui_frame_finish(ctx);
        kui_ctx_free(ctx);
    }

    /// A context that borrows a frame refuses a plugin instead of taking
    /// one into a list nothing fills. The path is the one a script would
    /// take (`examples/lua/features/slots/panel.lua` loads from its view), so the answer
    /// has to be a reason and not a quiet `true`.
    #[test]
    fn a_borrowed_frame_will_not_take_an_extension() {
        let mut core = kui_core::Core::new();
        let mut ui = core.frame(kui_core::Size::new(200.0, 100.0), 1.0);
        let mut borrowed = KuiCtx::borrowing_in(&mut ui);
        let ctx: *mut KuiCtx = &mut borrowed;

        // The library itself is beside the point: this is refused before
        // anything is opened, so even one that would load is.
        let lib = crate::ext::tests::a_library_with_no_kui_symbols();
        assert!(!kui_ctx_add_extension(ctx, ks("libc"), ks(lib)));
        assert_eq!(kui_ctx_extension_count(ctx), 0);

        let mut out = ks("");
        assert!(kui_ctx_extension_error(ctx, &mut out));
        let msg =
            std::str::from_utf8(unsafe { std::slice::from_raw_parts(out.ptr, out.len) }).unwrap();
        assert!(
            msg.contains("borrows a frame"),
            "the reason should name the borrowed frame, got {msg:?}"
        );
    }
}