Skip to main content

whiteout/
interfaces.rs

1// SPDX-License-Identifier: BSD-3-Clause
2// Copyright (c) 2026 Fernando Sahmkow
3//
4// Host-implemented interfaces: Rust traits the C++ library calls *into*.
5// Mirrors <whiteout/interfaces.h>. The generated `host` module is the other
6// direction — concrete implementations the library provides.
7//
8// Hand-written rather than generated. The C ABI side is a function-pointer
9// table plus a `void* userdata` (see `bindings/c/whiteout_host_shims.cpp`),
10// which maps onto a boxed trait object almost exactly — but the thunks need
11// panic containment and per-method marshalling that is not worth teaching a
12// generator for a handful of interfaces.
13
14use core::ffi::{c_char, c_void};
15use core::panic::AssertUnwindSafe;
16
17use crate::support::{RawBytes, RawCString};
18
19// ── Panic containment ─────────────────────────────────────────────────────
20//
21// The C ABI is compiled `-fno-exceptions`. A Rust panic unwinding into it is
22// undefined behaviour, so every thunk that can reach user code stops
23// unwinding at the boundary. This is a hazard neither the C# nor the Java
24// binding has, and it belongs in code rather than in a doc comment.
25
26fn guard<T>(fallback: T, f: impl FnOnce() -> T) -> T {
27    match std::panic::catch_unwind(AssertUnwindSafe(f)) {
28        Ok(v) => v,
29        Err(_) => {
30            // The payload has already gone to the panic hook.
31            eprintln!(
32                "whiteout: a panic in a host-implemented interface was contained \
33                 at the FFI boundary; the operation reports failure"
34            );
35            fallback
36        }
37    }
38}
39
40/// Hand a Rust-allocated buffer to C++, which copies it and immediately
41/// calls [`free_buffer`] with the same pointer — and *only* the pointer.
42///
43/// Rust needs the length to deallocate, so the allocation carries a length
44/// header and the shim sees a pointer just past it. A thread-local would
45/// also "work" given the copy-then-free-immediately contract, but it would
46/// break the moment two buffers were in flight; this cannot.
47const BUF_HEADER: usize = 16; // keeps the payload 16-byte aligned
48
49fn leak_buffer(data: Vec<u8>, out_data: *mut *mut u8, out_size: *mut usize) {
50    // SAFETY: both out-pointers come from the C++ caller's stack.
51    unsafe {
52        *out_data = core::ptr::null_mut();
53        *out_size = 0;
54    }
55    if data.is_empty() {
56        return;
57    }
58    let len = data.len();
59    let Ok(layout) = std::alloc::Layout::from_size_align(BUF_HEADER + len, BUF_HEADER) else {
60        return;
61    };
62    // SAFETY: non-zero size, valid alignment.
63    let base = unsafe { std::alloc::alloc(layout) };
64    if base.is_null() {
65        return;
66    }
67    // SAFETY: `base` owns `BUF_HEADER + len` bytes, and the header is
68    // aligned for `usize` because the block is 16-byte aligned.
69    unsafe {
70        (base as *mut usize).write(len);
71        core::ptr::copy_nonoverlapping(data.as_ptr(), base.add(BUF_HEADER), len);
72        *out_data = base.add(BUF_HEADER);
73        *out_size = len;
74    }
75}
76
77unsafe extern "C" fn free_buffer(data: *mut u8) {
78    if data.is_null() {
79        return;
80    }
81    // SAFETY: `data` is exactly what `leak_buffer` produced, so the header
82    // sits `BUF_HEADER` bytes below it and records the payload length.
83    unsafe {
84        let base = data.sub(BUF_HEADER);
85        let len = (base as *const usize).read();
86        let layout = std::alloc::Layout::from_size_align_unchecked(BUF_HEADER + len, BUF_HEADER);
87        std::alloc::dealloc(base, layout);
88    }
89}
90
91/// # Safety
92/// `p`/`len` must describe a byte run valid for `'a`.
93unsafe fn str_of<'a>(p: *const c_char, len: usize) -> &'a str {
94    if p.is_null() || len == 0 {
95        return "";
96    }
97    // SAFETY: the C++ side passes a `std::string`'s data and size.
98    let bytes = unsafe { core::slice::from_raw_parts(p as *const u8, len) };
99    core::str::from_utf8(bytes).unwrap_or("")
100}
101
102/// # Safety
103/// `data`/`size` must describe a byte run valid for `'a`.
104unsafe fn bytes_of<'a>(data: *const u8, size: usize) -> &'a [u8] {
105    if data.is_null() || size == 0 {
106        return &[];
107    }
108    // SAFETY: contract above.
109    unsafe { core::slice::from_raw_parts(data, size) }
110}
111
112// ── VirtualPathFileSystem ─────────────────────────────────────────────────
113
114/// A file system the library resolves by path.
115///
116/// `Send + Sync` is mandatory, not conservative: the C++ header documents
117/// that these methods may be called concurrently from worker threads.
118pub trait FileSystem: Send + Sync {
119    fn read_file(&self, path: &str) -> Option<Vec<u8>>;
120
121    fn write_file(&self, _path: &str, _data: &[u8]) -> bool {
122        false
123    }
124
125    fn file_exists(&self, path: &str) -> bool {
126        self.read_file(path).is_some()
127    }
128}
129
130#[repr(C)]
131struct VfsFnTable {
132    read_file: unsafe extern "C" fn(*mut c_void, *const c_char, usize, *mut *mut u8, *mut usize),
133    free_buffer: unsafe extern "C" fn(*mut u8),
134    write_file: unsafe extern "C" fn(*mut c_void, *const c_char, usize, *const u8, usize) -> i32,
135    file_exists: unsafe extern "C" fn(*mut c_void, *const c_char, usize) -> i32,
136}
137
138/// # Safety
139/// `userdata` must be the pointer `HostFileSystem::new` created.
140unsafe fn vfs_of<'a>(userdata: *mut c_void) -> &'a dyn FileSystem {
141    // SAFETY: contract above.
142    unsafe { &**(userdata as *const Box<dyn FileSystem>) }
143}
144
145unsafe extern "C" fn vfs_read_file(
146    userdata: *mut c_void,
147    path: *const c_char,
148    path_len: usize,
149    out_data: *mut *mut u8,
150    out_size: *mut usize,
151) {
152    let data = guard(Vec::new(), || {
153        // SAFETY: contracts above.
154        let fs = unsafe { vfs_of(userdata) };
155        let path = unsafe { str_of(path, path_len) };
156        fs.read_file(path).unwrap_or_default()
157    });
158    leak_buffer(data, out_data, out_size);
159}
160
161unsafe extern "C" fn vfs_write_file(
162    userdata: *mut c_void,
163    path: *const c_char,
164    path_len: usize,
165    data: *const u8,
166    size: usize,
167) -> i32 {
168    guard(0, || {
169        // SAFETY: contracts above.
170        let fs = unsafe { vfs_of(userdata) };
171        let path = unsafe { str_of(path, path_len) };
172        let bytes = unsafe { bytes_of(data, size) };
173        i32::from(fs.write_file(path, bytes))
174    })
175}
176
177unsafe extern "C" fn vfs_file_exists(
178    userdata: *mut c_void,
179    path: *const c_char,
180    path_len: usize,
181) -> i32 {
182    guard(0, || {
183        // SAFETY: contracts above.
184        let fs = unsafe { vfs_of(userdata) };
185        let path = unsafe { str_of(path, path_len) };
186        i32::from(fs.file_exists(path))
187    })
188}
189
190extern "C" {
191    fn whiteout_hostimpl_VirtualPathFileSystem_create(
192        userdata: *mut c_void,
193        fns: *const VfsFnTable,
194    ) -> *mut c_void;
195    fn whiteout_hostimpl_VirtualPathFileSystem_delete(handle: *mut c_void);
196
197    fn whiteout_hostimpl_test_VirtualPathFileSystem_readFile(
198        handle: *mut c_void,
199        path: *const c_char,
200    ) -> RawBytes;
201    fn whiteout_hostimpl_test_VirtualPathFileSystem_fileExists(
202        handle: *mut c_void,
203        path: *const c_char,
204    ) -> i32;
205}
206
207/// A [`FileSystem`] handed to the library.
208///
209/// Owns both the boxed trait object and the C++ subclass that forwards into
210/// it, so it must outlive every library call that uses it.
211pub struct HostFileSystem {
212    handle: *mut c_void,
213    userdata: *mut Box<dyn FileSystem>,
214}
215
216impl HostFileSystem {
217    pub fn new<F: FileSystem + 'static>(fs: F) -> Self {
218        let boxed: Box<dyn FileSystem> = Box::new(fs);
219        let userdata = Box::into_raw(Box::new(boxed));
220        let table = VfsFnTable {
221            read_file: vfs_read_file,
222            free_buffer,
223            write_file: vfs_write_file,
224            file_exists: vfs_file_exists,
225        };
226        // SAFETY: the shim copies the table, and `userdata` lives until drop.
227        let handle = unsafe {
228            whiteout_hostimpl_VirtualPathFileSystem_create(userdata as *mut c_void, &table)
229        };
230        HostFileSystem { handle, userdata }
231    }
232
233    /// Raw `interfaces::VirtualPathFileSystem*`, for library calls that
234    /// take one.
235    pub fn as_ptr(&self) -> *mut c_void {
236        self.handle
237    }
238
239    /// Read back *through the C++ interface* — the path library code takes.
240    pub fn read_through_native(&self, path: &str) -> Option<Vec<u8>> {
241        let c = std::ffi::CString::new(path).ok()?;
242        // SAFETY: `handle` is live for `&self`.
243        let raw = unsafe {
244            whiteout_hostimpl_test_VirtualPathFileSystem_readFile(self.handle, c.as_ptr())
245        };
246        // SAFETY: the invoker transfers ownership when non-empty.
247        unsafe { crate::support::Bytes::from_raw(raw) }.map(|b| b.to_vec())
248    }
249
250    pub fn exists_through_native(&self, path: &str) -> bool {
251        let Ok(c) = std::ffi::CString::new(path) else {
252            return false;
253        };
254        // SAFETY: `handle` is live for `&self`.
255        unsafe {
256            whiteout_hostimpl_test_VirtualPathFileSystem_fileExists(self.handle, c.as_ptr()) != 0
257        }
258    }
259}
260
261impl Drop for HostFileSystem {
262    fn drop(&mut self) {
263        // SAFETY: both pointers were produced in `new` and are freed once.
264        unsafe {
265            whiteout_hostimpl_VirtualPathFileSystem_delete(self.handle);
266            drop(Box::from_raw(self.userdata));
267        }
268    }
269}
270
271impl core::fmt::Debug for HostFileSystem {
272    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
273        f.debug_struct("HostFileSystem").finish_non_exhaustive()
274    }
275}
276
277// SAFETY: `FileSystem` requires `Send + Sync`; the handle is a plain heap
278// pointer with no thread affinity.
279unsafe impl Send for HostFileSystem {}
280unsafe impl Sync for HostFileSystem {}
281
282// ── CascFileSystem ────────────────────────────────────────────────────────
283
284/// A file system the library resolves by numeric data ID.
285///
286/// Required by the M2 parser and by CASC-backed asset loading.
287pub trait CascFileSystem: Send + Sync {
288    fn read_file(&self, file_id: u32) -> Option<Vec<u8>>;
289
290    fn reserve_file_id(&self, _path: &str) -> Option<u32> {
291        None
292    }
293
294    fn write_file(&self, _file_id: u32, _data: &[u8]) -> bool {
295        false
296    }
297
298    fn file_exists(&self, file_id: u32) -> bool {
299        self.read_file(file_id).is_some()
300    }
301}
302
303#[repr(C)]
304struct CascFsFnTable {
305    read_file: unsafe extern "C" fn(*mut c_void, u32, *mut *mut u8, *mut usize),
306    free_buffer: unsafe extern "C" fn(*mut u8),
307    reserve_file_id: unsafe extern "C" fn(*mut c_void, *const c_char, usize, *mut u32) -> i32,
308    write_file: unsafe extern "C" fn(*mut c_void, u32, *const u8, usize) -> i32,
309    file_exists: unsafe extern "C" fn(*mut c_void, u32) -> i32,
310}
311
312/// # Safety
313/// `userdata` must be the pointer `HostCascFileSystem::new` created.
314unsafe fn casc_of<'a>(userdata: *mut c_void) -> &'a dyn CascFileSystem {
315    // SAFETY: contract above.
316    unsafe { &**(userdata as *const Box<dyn CascFileSystem>) }
317}
318
319unsafe extern "C" fn casc_read_file(
320    userdata: *mut c_void,
321    file_id: u32,
322    out_data: *mut *mut u8,
323    out_size: *mut usize,
324) {
325    let data = guard(Vec::new(), || {
326        // SAFETY: contract above.
327        unsafe { casc_of(userdata) }
328            .read_file(file_id)
329            .unwrap_or_default()
330    });
331    leak_buffer(data, out_data, out_size);
332}
333
334unsafe extern "C" fn casc_reserve_file_id(
335    userdata: *mut c_void,
336    path: *const c_char,
337    path_len: usize,
338    out_id: *mut u32,
339) -> i32 {
340    guard(0, || {
341        // SAFETY: contracts above.
342        let fs = unsafe { casc_of(userdata) };
343        let path = unsafe { str_of(path, path_len) };
344        match fs.reserve_file_id(path) {
345            Some(id) => {
346                // SAFETY: the C++ caller supplies a valid out-pointer.
347                unsafe { *out_id = id };
348                1
349            }
350            None => 0,
351        }
352    })
353}
354
355unsafe extern "C" fn casc_write_file(
356    userdata: *mut c_void,
357    file_id: u32,
358    data: *const u8,
359    size: usize,
360) -> i32 {
361    guard(0, || {
362        // SAFETY: contracts above.
363        let fs = unsafe { casc_of(userdata) };
364        let bytes = unsafe { bytes_of(data, size) };
365        i32::from(fs.write_file(file_id, bytes))
366    })
367}
368
369unsafe extern "C" fn casc_file_exists(userdata: *mut c_void, file_id: u32) -> i32 {
370    guard(0, || {
371        // SAFETY: contract above.
372        i32::from(unsafe { casc_of(userdata) }.file_exists(file_id))
373    })
374}
375
376extern "C" {
377    fn whiteout_hostimpl_CascFileSystem_create(
378        userdata: *mut c_void,
379        fns: *const CascFsFnTable,
380    ) -> *mut c_void;
381    fn whiteout_hostimpl_CascFileSystem_delete(handle: *mut c_void);
382
383    fn whiteout_hostimpl_test_CascFileSystem_readFile(handle: *mut c_void, id: u32) -> RawBytes;
384    fn whiteout_hostimpl_test_CascFileSystem_fileExists(handle: *mut c_void, id: u32) -> i32;
385}
386
387/// A [`CascFileSystem`] handed to the library.
388pub struct HostCascFileSystem {
389    handle: *mut c_void,
390    userdata: *mut Box<dyn CascFileSystem>,
391}
392
393impl HostCascFileSystem {
394    pub fn new<F: CascFileSystem + 'static>(fs: F) -> Self {
395        let boxed: Box<dyn CascFileSystem> = Box::new(fs);
396        let userdata = Box::into_raw(Box::new(boxed));
397        let table = CascFsFnTable {
398            read_file: casc_read_file,
399            free_buffer,
400            reserve_file_id: casc_reserve_file_id,
401            write_file: casc_write_file,
402            file_exists: casc_file_exists,
403        };
404        // SAFETY: as `HostFileSystem::new`.
405        let handle =
406            unsafe { whiteout_hostimpl_CascFileSystem_create(userdata as *mut c_void, &table) };
407        HostCascFileSystem { handle, userdata }
408    }
409
410    pub fn as_ptr(&self) -> *mut c_void {
411        self.handle
412    }
413
414    pub fn read_through_native(&self, file_id: u32) -> Option<Vec<u8>> {
415        // SAFETY: `handle` is live for `&self`.
416        let raw = unsafe { whiteout_hostimpl_test_CascFileSystem_readFile(self.handle, file_id) };
417        // SAFETY: the invoker transfers ownership when non-empty.
418        unsafe { crate::support::Bytes::from_raw(raw) }.map(|b| b.to_vec())
419    }
420
421    pub fn exists_through_native(&self, file_id: u32) -> bool {
422        // SAFETY: `handle` is live for `&self`.
423        unsafe { whiteout_hostimpl_test_CascFileSystem_fileExists(self.handle, file_id) != 0 }
424    }
425}
426
427impl Drop for HostCascFileSystem {
428    fn drop(&mut self) {
429        // SAFETY: both pointers were produced in `new` and are freed once.
430        unsafe {
431            whiteout_hostimpl_CascFileSystem_delete(self.handle);
432            drop(Box::from_raw(self.userdata));
433        }
434    }
435}
436
437impl core::fmt::Debug for HostCascFileSystem {
438    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
439        f.debug_struct("HostCascFileSystem").finish_non_exhaustive()
440    }
441}
442
443// SAFETY: as `HostFileSystem`.
444unsafe impl Send for HostCascFileSystem {}
445unsafe impl Sync for HostCascFileSystem {}
446
447// ── HttpHandler ───────────────────────────────────────────────────────────
448
449/// Capability flags an [`HttpHandler`] may report.
450pub mod http_capability {
451    /// No optional capabilities.
452    pub const NONE: u32 = 0;
453    /// Connection multiplexing (HTTP/2).
454    pub const HTTP2_MULTIPLEXING: u32 = 0x1;
455}
456
457/// The one-shot reply channel handed to an [`HttpHandler`].
458///
459/// Consuming it with [`respond`](Self::respond) or [`fail`](Self::fail)
460/// fires the C++ callback exactly once. Dropping it without replying
461/// cancels the request with a transport error rather than leaving library
462/// code waiting forever — which is why the type owns the handle rather than
463/// exposing it.
464///
465/// `Send`, so a handler may hand it to a worker thread or an async runtime
466/// and reply later.
467pub struct HttpResponder {
468    handle: *mut c_void,
469}
470
471impl HttpResponder {
472    /// Deliver a response.
473    pub fn respond(self, status: i32, body: &[u8]) {
474        let me = core::mem::ManuallyDrop::new(self);
475        // SAFETY: the handle is live and fired exactly once — `self` is
476        // consumed and `Drop` suppressed.
477        unsafe {
478            whiteout_hostimpl_HttpResponseCallback_fire(
479                me.handle,
480                status,
481                body.as_ptr(),
482                body.len(),
483                core::ptr::null(),
484            );
485        }
486    }
487
488    /// Report a transport-level failure.
489    pub fn fail(self, error: &str) {
490        let me = core::mem::ManuallyDrop::new(self);
491        let c = std::ffi::CString::new(error).unwrap_or_default();
492        // SAFETY: as `respond`.
493        unsafe {
494            whiteout_hostimpl_HttpResponseCallback_fire(
495                me.handle,
496                0,
497                core::ptr::null(),
498                0,
499                c.as_ptr(),
500            );
501        }
502    }
503}
504
505impl Drop for HttpResponder {
506    fn drop(&mut self) {
507        // Only reached when the handler never replied. Cancelling fires a
508        // transport error, so waiting library code fails cleanly instead of
509        // hanging.
510        // SAFETY: the handle is live and has not been fired.
511        unsafe { whiteout_hostimpl_HttpResponseCallback_cancel(self.handle) };
512    }
513}
514
515impl core::fmt::Debug for HttpResponder {
516    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
517        f.debug_struct("HttpResponder").finish_non_exhaustive()
518    }
519}
520
521// SAFETY: the handle is a heap `std::function` with no thread affinity, and
522// `HttpResponder` owns it exclusively.
523unsafe impl Send for HttpResponder {}
524
525/// An HTTP client the library uses to fetch CDN data.
526///
527/// Both methods are asynchronous: reply through the [`HttpResponder`] when
528/// the request finishes, from whichever thread you like. Replying
529/// synchronously inside the method is also fine.
530pub trait HttpHandler: Send + Sync {
531    fn capabilities(&self) -> u32 {
532        http_capability::NONE
533    }
534
535    fn get(&self, url: &str, responder: HttpResponder);
536
537    /// Inclusive byte range.
538    fn get_range(&self, url: &str, start: u64, end: u64, responder: HttpResponder);
539}
540
541#[repr(C)]
542struct HttpFnTable {
543    capabilities: unsafe extern "C" fn(*mut c_void) -> u32,
544    get_async: unsafe extern "C" fn(*mut c_void, *const c_char, usize, *mut c_void),
545    get_range_async: unsafe extern "C" fn(*mut c_void, *const c_char, usize, u64, u64, *mut c_void),
546}
547
548/// # Safety
549/// `userdata` must be the pointer `HostHttpHandler::new` created.
550unsafe fn http_of<'a>(userdata: *mut c_void) -> &'a dyn HttpHandler {
551    // SAFETY: contract above.
552    unsafe { &**(userdata as *const Box<dyn HttpHandler>) }
553}
554
555unsafe extern "C" fn http_capabilities(userdata: *mut c_void) -> u32 {
556    guard(http_capability::NONE, || {
557        // SAFETY: contract above.
558        unsafe { http_of(userdata) }.capabilities()
559    })
560}
561
562unsafe extern "C" fn http_get_async(
563    userdata: *mut c_void,
564    url: *const c_char,
565    url_len: usize,
566    callback: *mut c_void,
567) {
568    let responder = HttpResponder { handle: callback };
569    // A panic drops the responder, which cancels the request — so the
570    // caller sees a transport error rather than waiting on a callback that
571    // will never fire.
572    guard((), move || {
573        // SAFETY: contracts above.
574        let h = unsafe { http_of(userdata) };
575        let url = unsafe { str_of(url, url_len) };
576        h.get(url, responder);
577    });
578}
579
580unsafe extern "C" fn http_get_range_async(
581    userdata: *mut c_void,
582    url: *const c_char,
583    url_len: usize,
584    start: u64,
585    end: u64,
586    callback: *mut c_void,
587) {
588    let responder = HttpResponder { handle: callback };
589    guard((), move || {
590        // SAFETY: contracts above.
591        let h = unsafe { http_of(userdata) };
592        let url = unsafe { str_of(url, url_len) };
593        h.get_range(url, start, end, responder);
594    });
595}
596
597extern "C" {
598    fn whiteout_hostimpl_HttpHandler_create(
599        userdata: *mut c_void,
600        fns: *const HttpFnTable,
601    ) -> *mut c_void;
602    fn whiteout_hostimpl_HttpHandler_delete(handle: *mut c_void);
603    fn whiteout_hostimpl_HttpResponseCallback_fire(
604        callback: *mut c_void,
605        status: i32,
606        body: *const u8,
607        body_len: usize,
608        error: *const c_char,
609    );
610    fn whiteout_hostimpl_HttpResponseCallback_cancel(callback: *mut c_void);
611
612    fn whiteout_hostimpl_test_HttpHandler_capabilities(handle: *mut c_void) -> u32;
613    fn whiteout_hostimpl_test_HttpHandler_getAsync(
614        handle: *mut c_void,
615        url: *const c_char,
616        out_status: *mut i32,
617        out_body: *mut RawBytes,
618        out_error: *mut RawCString,
619    );
620    fn whiteout_hostimpl_test_HttpHandler_getRangeAsync(
621        handle: *mut c_void,
622        url: *const c_char,
623        start: u64,
624        end: u64,
625        out_status: *mut i32,
626        out_body: *mut RawBytes,
627        out_error: *mut RawCString,
628    );
629}
630
631/// The result of driving a handler through the C++ interface.
632#[derive(Debug, Clone, PartialEq, Eq)]
633pub struct HttpOutcome {
634    pub status: i32,
635    pub body: Vec<u8>,
636    pub error: String,
637}
638
639fn empty_bytes() -> RawBytes {
640    RawBytes {
641        data: core::ptr::null(),
642        size: 0,
643        owner: core::ptr::null_mut(),
644    }
645}
646
647fn empty_cstring() -> RawCString {
648    RawCString {
649        chars: core::ptr::null(),
650        length: 0,
651        owner: core::ptr::null_mut(),
652    }
653}
654
655/// # Safety
656/// `body`/`error` must have been filled by one of the test invokers, which
657/// transfer ownership of both buffers.
658unsafe fn collect_outcome(status: i32, body: RawBytes, error: RawCString) -> HttpOutcome {
659    // SAFETY: contract above.
660    let body = unsafe { crate::support::Bytes::from_raw(body) }
661        .map(|b| b.to_vec())
662        .unwrap_or_default();
663    // SAFETY: contract above.
664    let error = unsafe { crate::support::take_string_opt(error) }.unwrap_or_default();
665    HttpOutcome {
666        status,
667        body,
668        error,
669    }
670}
671
672/// An [`HttpHandler`] handed to the library.
673pub struct HostHttpHandler {
674    handle: *mut c_void,
675    userdata: *mut Box<dyn HttpHandler>,
676}
677
678impl HostHttpHandler {
679    pub fn new<H: HttpHandler + 'static>(handler: H) -> Self {
680        let boxed: Box<dyn HttpHandler> = Box::new(handler);
681        let userdata = Box::into_raw(Box::new(boxed));
682        let table = HttpFnTable {
683            capabilities: http_capabilities,
684            get_async: http_get_async,
685            get_range_async: http_get_range_async,
686        };
687        // SAFETY: the shim copies the table; `userdata` lives until drop.
688        let handle =
689            unsafe { whiteout_hostimpl_HttpHandler_create(userdata as *mut c_void, &table) };
690        HostHttpHandler { handle, userdata }
691    }
692
693    /// Raw `interfaces::HttpHandler*`, for library calls that take one.
694    pub fn as_ptr(&self) -> *mut c_void {
695        self.handle
696    }
697
698    pub fn capabilities_through_native(&self) -> u32 {
699        // SAFETY: `handle` is live for `&self`.
700        unsafe { whiteout_hostimpl_test_HttpHandler_capabilities(self.handle) }
701    }
702
703    /// Drive `getAsync` and collect the reply.
704    ///
705    /// # Testing only
706    ///
707    /// The native invoker behind this captures the response into a **stack
708    /// local** and returns as soon as `getAsync` does. A handler that
709    /// replies after returning therefore writes into a dead frame. Real
710    /// library call sites own the callback properly and may be replied to
711    /// whenever; this helper exists so tests can drive a handler without
712    /// standing up a CDN, and a handler used with it must reply before
713    /// returning.
714    pub fn get_through_native(&self, url: &str) -> HttpOutcome {
715        let c = std::ffi::CString::new(url).unwrap_or_default();
716        let mut status = 0i32;
717        let mut body = empty_bytes();
718        let mut error = empty_cstring();
719        // SAFETY: all three out-pointers are live locals.
720        unsafe {
721            whiteout_hostimpl_test_HttpHandler_getAsync(
722                self.handle,
723                c.as_ptr(),
724                &mut status,
725                &mut body,
726                &mut error,
727            );
728            collect_outcome(status, body, error)
729        }
730    }
731
732    pub fn get_range_through_native(&self, url: &str, start: u64, end: u64) -> HttpOutcome {
733        let c = std::ffi::CString::new(url).unwrap_or_default();
734        let mut status = 0i32;
735        let mut body = empty_bytes();
736        let mut error = empty_cstring();
737        // SAFETY: as above.
738        unsafe {
739            whiteout_hostimpl_test_HttpHandler_getRangeAsync(
740                self.handle,
741                c.as_ptr(),
742                start,
743                end,
744                &mut status,
745                &mut body,
746                &mut error,
747            );
748            collect_outcome(status, body, error)
749        }
750    }
751}
752
753impl Drop for HostHttpHandler {
754    fn drop(&mut self) {
755        // SAFETY: both pointers were produced in `new` and are freed once.
756        unsafe {
757            whiteout_hostimpl_HttpHandler_delete(self.handle);
758            drop(Box::from_raw(self.userdata));
759        }
760    }
761}
762
763impl core::fmt::Debug for HostHttpHandler {
764    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
765        f.debug_struct("HostHttpHandler").finish_non_exhaustive()
766    }
767}
768
769// SAFETY: as `HostFileSystem`.
770unsafe impl Send for HostHttpHandler {}
771unsafe impl Sync for HostHttpHandler {}
772
773// ── WorkerPool ────────────────────────────────────────────────────────────
774
775/// A unit of work submitted by the library.
776///
777/// Run it with [`run`](Self::run), from whatever thread the pool chooses.
778/// Dropping it without running cancels the work — which is safe but will
779/// stall anything waiting on the task's signal semaphore, so prefer
780/// running it.
781///
782/// `Send` so it can be moved onto a worker thread; deliberately not `Sync`,
783/// since it may only be run once.
784pub struct WorkerTask {
785    fn_handle: *mut c_void,
786    wait: Option<(*mut c_void, u64)>,
787    signal: Option<(*mut c_void, u64)>,
788}
789
790impl WorkerTask {
791    /// Wait on the task's semaphore if it has one, run the work, then
792    /// signal. This is the whole contract a pool implementation owes.
793    pub fn run(self) {
794        let me = core::mem::ManuallyDrop::new(self);
795        // SAFETY: each handle is live, and `fire` consumes the function
796        // exactly once because `self` is consumed and `Drop` suppressed.
797        unsafe {
798            if let Some((sem, value)) = me.wait {
799                whiteout_hostimpl_TimelineSemaphore_await(sem, value);
800            }
801            whiteout_hostimpl_WorkerTaskFn_fire(me.fn_handle);
802            if let Some((sem, value)) = me.signal {
803                whiteout_hostimpl_TimelineSemaphore_signal(sem, value);
804            }
805        }
806    }
807}
808
809impl Drop for WorkerTask {
810    fn drop(&mut self) {
811        // Only reached when the pool declined to run the task.
812        // SAFETY: the function has not been fired.
813        unsafe { whiteout_hostimpl_WorkerTaskFn_cancel(self.fn_handle) };
814    }
815}
816
817impl core::fmt::Debug for WorkerTask {
818    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
819        f.debug_struct("WorkerTask").finish_non_exhaustive()
820    }
821}
822
823// SAFETY: the handles are heap objects with no thread affinity; the task
824// owns them exclusively and runs at most once.
825unsafe impl Send for WorkerTask {}
826
827/// A thread pool the library submits work to.
828pub trait WorkerPool: Send + Sync {
829    /// Run `task` — now, or on a worker thread.
830    fn submit(&self, task: WorkerTask);
831
832    /// Block until every submitted task has finished.
833    fn wait_idle(&self);
834
835    fn thread_count(&self) -> usize;
836}
837
838#[repr(C)]
839struct WorkerTaskFlat {
840    fn_handle: *mut c_void,
841    wait_semaphore: *mut c_void,
842    wait_value: u64,
843    signal_semaphore: *mut c_void,
844    signal_value: u64,
845}
846
847#[repr(C)]
848struct WorkerPoolFnTable {
849    submit: unsafe extern "C" fn(*mut c_void, *const WorkerTaskFlat),
850    wait_idle: unsafe extern "C" fn(*mut c_void),
851    thread_count: unsafe extern "C" fn(*mut c_void) -> usize,
852}
853
854/// # Safety
855/// `userdata` must be the pointer `HostWorkerPool::new` created.
856unsafe fn pool_of<'a>(userdata: *mut c_void) -> &'a dyn WorkerPool {
857    // SAFETY: contract above.
858    unsafe { &**(userdata as *const Box<dyn WorkerPool>) }
859}
860
861unsafe extern "C" fn pool_submit(userdata: *mut c_void, flat: *const WorkerTaskFlat) {
862    if flat.is_null() {
863        return;
864    }
865    // SAFETY: the shim passes a live stack struct.
866    let flat = unsafe { &*flat };
867    let task = WorkerTask {
868        fn_handle: flat.fn_handle,
869        wait: (!flat.wait_semaphore.is_null()).then_some((flat.wait_semaphore, flat.wait_value)),
870        signal: (!flat.signal_semaphore.is_null())
871            .then_some((flat.signal_semaphore, flat.signal_value)),
872    };
873    guard((), move || {
874        // SAFETY: contract above.
875        unsafe { pool_of(userdata) }.submit(task);
876    });
877}
878
879unsafe extern "C" fn pool_wait_idle(userdata: *mut c_void) {
880    guard((), || {
881        // SAFETY: contract above.
882        unsafe { pool_of(userdata) }.wait_idle();
883    });
884}
885
886unsafe extern "C" fn pool_thread_count(userdata: *mut c_void) -> usize {
887    guard(1, || {
888        // SAFETY: contract above.
889        unsafe { pool_of(userdata) }.thread_count()
890    })
891}
892
893extern "C" {
894    fn whiteout_hostimpl_WorkerPool_create(
895        userdata: *mut c_void,
896        fns: *const WorkerPoolFnTable,
897    ) -> *mut c_void;
898    fn whiteout_hostimpl_WorkerPool_delete(handle: *mut c_void);
899    fn whiteout_hostimpl_WorkerTaskFn_fire(fn_handle: *mut c_void);
900    fn whiteout_hostimpl_WorkerTaskFn_cancel(fn_handle: *mut c_void);
901    fn whiteout_hostimpl_TimelineSemaphore_await(sem: *mut c_void, value: u64);
902    fn whiteout_hostimpl_TimelineSemaphore_signal(sem: *mut c_void, value: u64);
903
904    fn whiteout_hostimpl_test_WorkerPool_threadCount(handle: *mut c_void) -> usize;
905    fn whiteout_hostimpl_test_WorkerPool_waitIdle(handle: *mut c_void);
906    fn whiteout_hostimpl_test_WorkerPool_submitIncrementSentinel(
907        handle: *mut c_void,
908        out_sentinel: *mut i32,
909    );
910}
911
912/// A [`WorkerPool`] handed to the library.
913pub struct HostWorkerPool {
914    handle: *mut c_void,
915    userdata: *mut Box<dyn WorkerPool>,
916}
917
918impl HostWorkerPool {
919    pub fn new<P: WorkerPool + 'static>(pool: P) -> Self {
920        let boxed: Box<dyn WorkerPool> = Box::new(pool);
921        let userdata = Box::into_raw(Box::new(boxed));
922        let table = WorkerPoolFnTable {
923            submit: pool_submit,
924            wait_idle: pool_wait_idle,
925            thread_count: pool_thread_count,
926        };
927        // SAFETY: the shim copies the table; `userdata` lives until drop.
928        let handle =
929            unsafe { whiteout_hostimpl_WorkerPool_create(userdata as *mut c_void, &table) };
930        HostWorkerPool { handle, userdata }
931    }
932
933    /// Raw `interfaces::WorkerPool*`, for library calls that take one.
934    pub fn as_ptr(&self) -> *mut c_void {
935        self.handle
936    }
937
938    pub fn thread_count_through_native(&self) -> usize {
939        // SAFETY: `handle` is live for `&self`.
940        unsafe { whiteout_hostimpl_test_WorkerPool_threadCount(self.handle) }
941    }
942
943    pub fn wait_idle_through_native(&self) {
944        // SAFETY: `handle` is live for `&self`.
945        unsafe { whiteout_hostimpl_test_WorkerPool_waitIdle(self.handle) }
946    }
947
948    /// Submit a task from the C++ side that increments `sentinel`.
949    ///
950    /// This is how the tests prove a submitted task actually reached the
951    /// Rust pool and ran: the closure lives in C++, so nothing but a real
952    /// round trip can move the counter.
953    pub fn submit_sentinel_through_native(&self, sentinel: &mut i32) {
954        // SAFETY: `sentinel` outlives the call, and the task runs before
955        // `waitIdle` returns.
956        unsafe {
957            whiteout_hostimpl_test_WorkerPool_submitIncrementSentinel(self.handle, sentinel);
958        }
959    }
960}
961
962impl Drop for HostWorkerPool {
963    fn drop(&mut self) {
964        // SAFETY: both pointers were produced in `new` and are freed once.
965        unsafe {
966            whiteout_hostimpl_WorkerPool_delete(self.handle);
967            drop(Box::from_raw(self.userdata));
968        }
969    }
970}
971
972impl core::fmt::Debug for HostWorkerPool {
973    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
974        f.debug_struct("HostWorkerPool").finish_non_exhaustive()
975    }
976}
977
978// SAFETY: as `HostFileSystem`.
979unsafe impl Send for HostWorkerPool {}
980unsafe impl Sync for HostWorkerPool {}