rust-lang-nursery / lazy-static.rs

A small macro for defining lazy evaluated static variables in Rust.
Apache License 2.0
1.9k stars 108 forks source link

Crossing FFI boundaries with raw pointers. #171

Open Ben-PH opened 4 years ago

Ben-PH commented 4 years ago

I'm building a system that calls into C libraries. One of the libraries handles device initialisation. It returns a pointer, and cannot be called more than once. Hence the use of lazy_static.

I have two questions, one is about assuring how to constrain construction across the lifetime of a system, and another about the contracts that come with unsafe impl Send/Sync for $raw_ptr

Here is a code snippet of what I'm dealing with

// ../rustlibs/serial/src/lib.rs
#![no_std]

#[derive(Copy, Clone)]
#[repr(C)]
struct SerialPtr(*const core::ffi::c_void);

extern "C" {
    fn serial_send(c_library_struct: *const core::ffi::c_void, ...) -> usize;
    fn serial_init() -> SerialPtr;
    fn serial_register_handler(serial: SerialPtr,  ...);
}

unsafe impl core::marker::Send for Serial {}
unsafe impl core::marker::Sync for Serial {}
pub struct Serial {
    ptr: SerialPtr
}

// essentially, SERIAL must be a singleton for the entire system due to serial_init call count.
lazy_static! {
    static ref SERIAL: Serial = unsafe {
        let res = Serial {
            ptr: serial_init(), // CRITICAL: must be called once, and once ONLY for entire system uptime
        };
        serial_register_handler(res.ptr, Some(default_handler));
        res
    };
}

impl Serial {
    pub fn send(msg: &[u8]) -> usize {
        unsafe { serial_send(SERIAL.ptr, ...) }
    }
    ...
}

Here is the process flow of my system:

  1. root-process start in C code-base (is always the first process after power-up, and last before power-down)
  2. system initialisation from C code-base (not including serial_init)
  3. spin up child process from C code-base(immediately sends request to root-process use of serial device. Blocks until root-process replys to request)
  4. root-process calls into Rust code-base
  5. root-proc recieves request from child
  6. part of processing this uses Serial::send

For context of how I currently use it from the static library that C calls:

// ../rustlibs/root_service/src/lib.rs
#![no_std]
#[crate_type = "static_lib"]
use serial::*;

#[no_mangle]
fn entry_point() -> ! {
    loop {
        match blocking_recv(ipc_source, msg) {
            // Derefs into SERIAL, thus init'ing the device on first match, replying with the result
            SERIAL_SEND => reply(ipc_source, Serial::send(msg)),
            ...
        }
    }
}

Regarding my question of limiting serial_init() invocation to once-only system wide: If I make serial a static lib, will that mean there will be a fresh serial_init() invoked for each executable that links it? Does the same apply if I make it an rlib? If I make it dylib, what would that do to duplicate SERIALs within system memory?

Regarding my question of marking Serial with Send + Sync, what can I do with respect to ensuring that it's safe?