Skip to main content

denise_ffi/
lib.rs

1//! Denise's C ABI: a `cdylib` for hosts that are not written in Rust.
2//!
3//! Denise's own backends are Rust and need none of this. This crate exists for
4//! the other direction — a Win32 control inside an MFC application, a WinForms or
5//! VB6 host reaching it through the ActiveX shim, an `NSView` in a Cocoa app, a
6//! Python or C# panel on an embedded box. All of them speak C.
7//!
8//! # The shape of it
9//!
10//! The host owns the window and the pixel buffer; Denise owns the widget tree and
11//! draws into whatever it is handed:
12//!
13//! ```c
14//! DeniseUi *ui = denise_ui_new(800, 480, DENISE_THEME_DARK);
15//! uint64_t root = denise_ui_root(ui);
16//! denise_ui_add_button(ui, root, (DeniseRect){20, 20, 160, 44}, "Save", 1, DENISE_ROLE_PRIMARY);
17//!
18//! /* per frame */
19//! denise_ui_tick(ui, now_ms);
20//! if (denise_ui_needs_paint(ui)) {
21//!     DeniseFrame frame = { pixels, len, w, h, stride, DENISE_FORMAT_XRGB8888, age };
22//!     denise_ui_paint(ui, &frame);
23//!     DeniseRect damage[16];
24//!     intptr_t n = denise_ui_damage(ui, damage, 16);
25//!     /* BitBlt only those rectangles */
26//!     denise_ui_presented(ui);
27//! }
28//! uint32_t message;
29//! while (denise_ui_poll_message(ui, &message)) { /* ... */ }
30//! ```
31//!
32//! There is no `Surface` here and no event loop. Both belong to the host, and a
33//! library that tried to own either would be unembeddable in exactly the places
34//! this is for.
35//!
36//! # Rules the whole ABI keeps
37//!
38//! - **Handles are opaque.** A `DeniseUi *` comes from [`denise_ui_new`] and goes
39//!   to [`denise_ui_free`]. Nothing else may free it.
40//! - **A node is a `uint64_t`**, and `0` is never a valid node. Ids carry a
41//!   generation, so an id kept past a [`denise_ui_remove`] fails to resolve rather
42//!   than addressing whoever took the slot.
43//! - **A message is a `uint32_t`**, chosen by the host, and `0` means *no
44//!   message*. A button given `0` emits nothing, and [`denise_ui_poll_message`]
45//!   never yields it. That is what lets a widget be created without one.
46//! - **Strings are NUL-terminated UTF-8** going in and coming out. Invalid UTF-8
47//!   is [`DENISE_ERR_INVALID`], not a replacement character: silently mangling a
48//!   host's text is worse than refusing it.
49//! - **A negative return is a status**, and every status has a message from
50//!   [`denise_status_message`].
51//! - **Nothing is thread-safe.** One `DeniseUi` belongs to one thread, which for
52//!   every host this targets is the UI thread it was created on.
53//!
54//! # Panics do not cross
55//!
56//! Every entry point catches unwinding and returns [`DENISE_ERR_PANIC`]. A panic
57//! is a bug in Denise, and a bug in Denise should not take down a host process
58//! that has unsaved work in three other windows. The call did nothing; the `Ui`
59//! it was called on should be treated as suspect and freed.
60//!
61//! # The header is the contract
62//!
63//! `include/denise.h` is written by hand, not generated. A generated header
64//! follows whatever the Rust happens to say this week, which is the opposite of
65//! what a stable ABI means — the header is the thing that must not move, and the
66//! Rust is what gets checked against it. [`tests/header.rs`](../tests/header.rs)
67//! does the checking: every exported symbol appears in both, with the same
68//! numbers for every key, role and constant.
69
70use std::collections::VecDeque;
71use std::ffi::{CStr, c_char};
72use std::panic::{AssertUnwindSafe, catch_unwind};
73
74use denise_ui::Ui;
75
76pub mod input;
77pub mod keys;
78pub mod paint;
79pub mod tree;
80pub mod types;
81
82// Flattened so a Rust caller of the `rlib` sees the same names in the same shape
83// as the header, rather than having to learn which module a symbol lives in when
84// C never has to. `keys` stays namespaced: 102 constants at the crate root would
85// bury everything else.
86pub use input::*;
87pub use paint::*;
88pub use tree::*;
89pub use types::*;
90
91/// Version of this ABI.
92///
93/// Bumped when a signature, a constant or the meaning of one changes. Added
94/// functions do not bump it. A host that checks nothing else should check this.
95pub const DENISE_ABI_VERSION: u32 = 1;
96
97/// The call succeeded.
98pub const DENISE_OK: i32 = 0;
99/// A required pointer was `NULL`.
100pub const DENISE_ERR_NULL: i32 = -1;
101/// An argument was out of range, or a string was not valid UTF-8.
102pub const DENISE_ERR_INVALID: i32 = -2;
103/// The node id does not name a live node.
104pub const DENISE_ERR_NO_NODE: i32 = -3;
105/// The buffer supplied is too small for the result.
106pub const DENISE_ERR_BUFFER_TOO_SMALL: i32 = -4;
107/// The widget named is not of the kind this call needs.
108pub const DENISE_ERR_WRONG_WIDGET: i32 = -5;
109/// A panic was caught. The call did nothing; treat the `DeniseUi` as suspect.
110pub const DENISE_ERR_PANIC: i32 = -6;
111
112/// The crate version, NUL-terminated and statically allocated.
113///
114/// The trailing NUL is concatenated in rather than written, so this cannot drift
115/// from `Cargo.toml`.
116static VERSION: &str = concat!(env!("CARGO_PKG_VERSION"), "\0");
117
118/// Denise's version as a NUL-terminated string. Never `NULL`, never freed.
119#[unsafe(no_mangle)]
120pub extern "C" fn denise_version() -> *const c_char {
121    VERSION.as_ptr().cast()
122}
123
124/// The ABI version this library was built with. See [`DENISE_ABI_VERSION`].
125#[unsafe(no_mangle)]
126pub extern "C" fn denise_abi_version() -> u32 {
127    DENISE_ABI_VERSION
128}
129
130/// A short description of a status code, NUL-terminated. Never `NULL`, never
131/// freed. An unrecognised code gets a generic message rather than `NULL`, because
132/// a host logging an error should not have to handle an error from the logger.
133#[unsafe(no_mangle)]
134pub extern "C" fn denise_status_message(status: i32) -> *const c_char {
135    let text: &CStr = match status {
136        DENISE_OK => c"ok",
137        DENISE_ERR_NULL => c"a required pointer was NULL",
138        DENISE_ERR_INVALID => c"an argument was out of range, or a string was not UTF-8",
139        DENISE_ERR_NO_NODE => c"the node id does not name a live node",
140        DENISE_ERR_BUFFER_TOO_SMALL => c"the buffer supplied is too small",
141        DENISE_ERR_WRONG_WIDGET => c"the node is not a widget of that kind",
142        DENISE_ERR_PANIC => c"denise panicked; the call did nothing",
143        _ => c"unknown status",
144    };
145    text.as_ptr()
146}
147
148/// An opaque user interface. Create with [`denise_ui_new`], destroy with
149/// [`denise_ui_free`].
150///
151/// Messages are `u32` because C has no generics and a host needs to choose its
152/// own vocabulary. They are queued here rather than read from
153/// [`Ui::drain_messages`] on demand so that `0` can be dropped on the way in and
154/// [`denise_ui_poll_message`] can keep its promise never to yield one.
155pub struct DeniseUi {
156    ui: Ui<u32>,
157    pending: VecDeque<u32>,
158}
159
160impl DeniseUi {
161    /// Moves anything the widgets emitted into the queue, discarding the `0`s.
162    fn collect(&mut self) {
163        self.pending
164            .extend(self.ui.drain_messages().filter(|&m| m != 0));
165    }
166}
167
168/// Runs `body`, turning a panic into `fallback`.
169///
170/// Unwinding into C is undefined behaviour. `extern "C"` aborts rather than
171/// unwind, which is sound but takes the host down with it; a status code lets the
172/// host log the bug and carry on with its other windows.
173///
174/// The `AssertUnwindSafe` is deliberate and is the reason [`DENISE_ERR_PANIC`]'s
175/// documentation says to treat the `Ui` as suspect: a panic part-way through a
176/// tree mutation can leave state a later call would observe. Better observable
177/// and recoverable than an aborted process.
178pub(crate) fn guard<T>(fallback: T, body: impl FnOnce() -> T) -> T {
179    match catch_unwind(AssertUnwindSafe(body)) {
180        Ok(value) => value,
181        Err(_) => fallback,
182    }
183}
184
185/// Borrows a handle, or `None` if it is `NULL`.
186///
187/// # Safety
188///
189/// `ui` must be `NULL`, or a pointer from [`denise_ui_new`] that has not been
190/// passed to [`denise_ui_free`].
191pub(crate) unsafe fn handle<'a>(ui: *mut DeniseUi) -> Option<&'a mut DeniseUi> {
192    // SAFETY: the caller promises the pointer is either null or one we handed out
193    // from `Box::into_raw` and still own. `as_mut` handles the null case, and the
194    // ABI's one-thread-per-`DeniseUi` rule is what makes the exclusive borrow
195    // sound.
196    unsafe { ui.as_mut() }
197}
198
199/// Reads a NUL-terminated UTF-8 string, or `None` if it is `NULL` or not UTF-8.
200///
201/// # Safety
202///
203/// `text` must be `NULL`, or point at a NUL-terminated byte string that stays
204/// valid and unwritten for the duration of the call.
205pub(crate) unsafe fn utf8<'a>(text: *const c_char) -> Option<&'a str> {
206    if text.is_null() {
207        return None;
208    }
209    // SAFETY: the caller promises a NUL-terminated string valid for the call, and
210    // the returned lifetime is only ever used within one.
211    unsafe { CStr::from_ptr(text) }.to_str().ok()
212}
213
214/// Creates a user interface `width` by `height` pixels, in one of the built-in
215/// themes ([`DENISE_THEME_DARK`] and friends).
216///
217/// Returns `NULL` if the size is empty or the theme is not one of them.
218#[unsafe(no_mangle)]
219pub extern "C" fn denise_ui_new(width: u32, height: u32, theme: u32) -> *mut DeniseUi {
220    guard(std::ptr::null_mut(), || {
221        let Some(theme) = types::theme(theme) else {
222            return std::ptr::null_mut();
223        };
224        if width == 0 || height == 0 {
225            return std::ptr::null_mut();
226        }
227        Box::into_raw(Box::new(DeniseUi {
228            ui: Ui::new(denise::Size::new(width, height), theme),
229            pending: VecDeque::new(),
230        }))
231    })
232}
233
234/// Creates a user interface with its theme metrics at a scale factor, given in
235/// hundredths — `150` is 1.5x, `200` a 2x display.
236///
237/// This scales the widgets' own furniture: control heights, corner radii,
238/// borders. The rectangles the host passes stay physical pixels, exactly as
239/// before — the host computes them, so the host multiplies them, which is the
240/// same one-place-multiplies rule the Rust API documents. A host reacting to a
241/// DPI change (`WM_DPICHANGED`) rebuilds with the new factor, as it already
242/// rebuilds on a theme change.
243///
244/// Returns `NULL` if the size is empty, the theme unknown, or the scale zero.
245#[unsafe(no_mangle)]
246pub extern "C" fn denise_ui_new_scaled(
247    width: u32,
248    height: u32,
249    theme: u32,
250    scale_x100: u32,
251) -> *mut DeniseUi {
252    guard(std::ptr::null_mut(), || {
253        let Some(theme) = types::theme(theme) else {
254            return std::ptr::null_mut();
255        };
256        if width == 0 || height == 0 || scale_x100 == 0 {
257            return std::ptr::null_mut();
258        }
259        let theme = theme.scaled(scale_x100 as f32 / 100.0);
260        Box::into_raw(Box::new(DeniseUi {
261            ui: Ui::new(denise::Size::new(width, height), theme),
262            pending: VecDeque::new(),
263        }))
264    })
265}
266
267/// Destroys a user interface. `NULL` is accepted and does nothing, as `free`
268/// does.
269///
270/// # Safety
271///
272/// `ui` must be `NULL`, or a pointer from [`denise_ui_new`] not already freed.
273/// Every node id taken from it is dead afterwards.
274#[unsafe(no_mangle)]
275pub unsafe extern "C" fn denise_ui_free(ui: *mut DeniseUi) {
276    if ui.is_null() {
277        return;
278    }
279    guard((), || {
280        // SAFETY: the caller promises this came from `denise_ui_new`, which built
281        // it with `Box::into_raw`, and that it has not been freed already.
282        drop(unsafe { Box::from_raw(ui) });
283    });
284}
285
286/// Switches to one of the built-in themes and repaints everything.
287///
288/// # Safety
289///
290/// `ui` must be a live handle from [`denise_ui_new`].
291#[unsafe(no_mangle)]
292pub unsafe extern "C" fn denise_ui_set_theme(ui: *mut DeniseUi, theme: u32) -> i32 {
293    guard(DENISE_ERR_PANIC, || {
294        // SAFETY: forwarding the caller's promise about `ui`.
295        let Some(handle) = (unsafe { handle(ui) }) else {
296            return DENISE_ERR_NULL;
297        };
298        let Some(theme) = types::theme(theme) else {
299            return DENISE_ERR_INVALID;
300        };
301        handle.ui.set_theme(theme);
302        DENISE_OK
303    })
304}
305
306/// Writes the surface size. Either output may be `NULL`.
307///
308/// # Safety
309///
310/// `ui` must be a live handle; `width` and `height` must be `NULL` or point at
311/// writable `uint32_t`s.
312#[unsafe(no_mangle)]
313pub unsafe extern "C" fn denise_ui_size(
314    ui: *mut DeniseUi,
315    width: *mut u32,
316    height: *mut u32,
317) -> i32 {
318    guard(DENISE_ERR_PANIC, || {
319        // SAFETY: forwarding the caller's promise about `ui`.
320        let Some(handle) = (unsafe { handle(ui) }) else {
321            return DENISE_ERR_NULL;
322        };
323        let size = handle.ui.size();
324        if !width.is_null() {
325            // SAFETY: the caller promises a writable `uint32_t` when not null.
326            unsafe { width.write(size.width) };
327        }
328        if !height.is_null() {
329            // SAFETY: as above.
330            unsafe { height.write(size.height) };
331        }
332        DENISE_OK
333    })
334}
335
336/// Takes the next message, or returns `false` if there is none.
337///
338/// Never yields `0`; see the crate documentation for why.
339///
340/// # Safety
341///
342/// `ui` must be a live handle; `out` must be `NULL` or point at a writable
343/// `uint32_t`.
344#[unsafe(no_mangle)]
345pub unsafe extern "C" fn denise_ui_poll_message(ui: *mut DeniseUi, out: *mut u32) -> bool {
346    guard(false, || {
347        // SAFETY: forwarding the caller's promise about `ui`.
348        let Some(handle) = (unsafe { handle(ui) }) else {
349            return false;
350        };
351        handle.collect();
352        let Some(message) = handle.pending.pop_front() else {
353            return false;
354        };
355        if !out.is_null() {
356            // SAFETY: the caller promises a writable `uint32_t` when not null.
357            unsafe { out.write(message) };
358        }
359        true
360    })
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn the_version_string_is_nul_terminated_and_matches_the_crate() {
369        assert!(VERSION.ends_with('\0'));
370        // SAFETY: `VERSION` is a static `&str` with exactly one NUL, at the end.
371        let text = unsafe { CStr::from_ptr(denise_version()) };
372        assert_eq!(text.to_str().unwrap(), env!("CARGO_PKG_VERSION"));
373    }
374
375    #[test]
376    fn every_status_has_its_own_message() {
377        let codes = [
378            DENISE_OK,
379            DENISE_ERR_NULL,
380            DENISE_ERR_INVALID,
381            DENISE_ERR_NO_NODE,
382            DENISE_ERR_BUFFER_TOO_SMALL,
383            DENISE_ERR_WRONG_WIDGET,
384            DENISE_ERR_PANIC,
385        ];
386        let mut seen: Vec<&str> = Vec::new();
387        for code in codes {
388            // SAFETY: `denise_status_message` returns a static C string.
389            let text = unsafe { CStr::from_ptr(denise_status_message(code)) }
390                .to_str()
391                .unwrap();
392            assert!(
393                !seen.contains(&text),
394                "{code} shares a message with another"
395            );
396            seen.push(text);
397        }
398        // SAFETY: as above.
399        let unknown = unsafe { CStr::from_ptr(denise_status_message(-999)) };
400        assert_eq!(unknown.to_str().unwrap(), "unknown status");
401    }
402
403    /// Every entry point must survive a `NULL` handle, because in a host that
404    /// reaches Denise through three layers of marshalling it will get one.
405    #[test]
406    fn a_null_handle_is_refused_rather_than_dereferenced() {
407        // SAFETY: passing null is exactly what the ABI documents as accepted.
408        unsafe {
409            denise_ui_free(std::ptr::null_mut());
410            assert_eq!(
411                denise_ui_set_theme(std::ptr::null_mut(), 0),
412                DENISE_ERR_NULL
413            );
414            assert_eq!(
415                denise_ui_size(
416                    std::ptr::null_mut(),
417                    std::ptr::null_mut(),
418                    std::ptr::null_mut()
419                ),
420                DENISE_ERR_NULL
421            );
422            assert!(!denise_ui_poll_message(
423                std::ptr::null_mut(),
424                std::ptr::null_mut()
425            ));
426        }
427    }
428
429    #[test]
430    fn a_bad_size_or_theme_produces_no_handle() {
431        assert!(denise_ui_new(0, 480, DENISE_THEME_DARK).is_null());
432        assert!(denise_ui_new(800, 0, DENISE_THEME_DARK).is_null());
433        assert!(denise_ui_new(800, 480, 99).is_null());
434
435        let ui = denise_ui_new(800, 480, DENISE_THEME_DARK);
436        assert!(!ui.is_null());
437        // SAFETY: `ui` came from `denise_ui_new` and is freed exactly once.
438        unsafe { denise_ui_free(ui) };
439    }
440}
441
442/// Compiles the examples in this crate's README, so they cannot drift from the API
443/// they claim to demonstrate. Never built except under `cargo test --doc`.
444#[cfg(doctest)]
445#[doc = include_str!("../README.md")]
446struct Readme;