spiral-sdk 0.1.10

Rust SDK for Spiral resource and table APIs
Documentation
//! Host-side ownership and async bridge for native URI resolution.
use std::ffi::{CStr, CString, c_char, c_void};
use std::panic::{AssertUnwindSafe, catch_unwind};
use std::sync::Arc;

use tokio::runtime::Handle;
use url::Url;

use super::raw::ScandalUriResolver;
use crate::scandal::error::ScandalError;
use crate::scandal::{ResolvedUri, ResourceAccess, UriResolver};

struct HostResolver {
    resolver: Arc<dyn UriResolver>,
    runtime: Handle,
}

pub(super) fn callbacks(
    resolver: Arc<dyn UriResolver>,
) -> Result<ScandalUriResolver, ScandalError> {
    let runtime = Handle::try_current().map_err(internal)?;
    let context = Box::into_raw(Box::new(HostResolver { resolver, runtime })).cast();
    Ok(ScandalUriResolver {
        context,
        resolve: Some(resolve),
        free_result: Some(free_result),
        release: Some(release),
    })
}

unsafe extern "C" fn resolve(
    context: *mut c_void,
    uri: *const c_char,
    access: *const c_char,
    out: *mut *mut c_char,
) -> bool {
    // SAFETY: The native resolver retains this context and calls from a blocking worker.
    let result = catch_unwind(AssertUnwindSafe(|| -> Result<ResolvedUri, ScandalError> {
        let host = unsafe { &*context.cast::<HostResolver>() };
        let uri = unsafe { CStr::from_ptr(uri) }.to_str().map_err(internal)?;
        let uri = Url::parse(uri).map_err(internal)?;
        let access: ResourceAccess =
            serde_json::from_slice(unsafe { CStr::from_ptr(access) }.to_bytes())
                .map_err(internal)?;
        host.runtime
            .block_on(host.resolver.resolve_for(&uri, access))
    }))
    .unwrap_or_else(|_| Err(internal("host URI resolver panicked")));
    let success = result.is_ok();
    let json = match result {
        Ok(value) => serde_json::to_string(&value).expect("URI binding serializes"),
        Err(error) => serde_json::to_string(&error.to_wire()).expect("error envelope serializes"),
    };
    // JSON escapes embedded NUL bytes, so this is always a C string.
    unsafe {
        *out = CString::new(json).expect("JSON has no NUL").into_raw();
    }
    success
}

unsafe extern "C" fn free_result(_context: *mut c_void, value: *mut c_char) {
    unsafe {
        drop(CString::from_raw(value));
    }
}

unsafe extern "C" fn release(context: *mut c_void) {
    unsafe {
        drop(Box::from_raw(context.cast::<HostResolver>()));
    }
}

fn internal(error: impl ToString) -> ScandalError {
    ScandalError::Internal {
        message: error.to_string(),
    }
}