DEV Community

Ben Lovy
Ben Lovy

Posted on

Rust Function Pointers Across FFI Boundary

Hi DEV - it's been ages! I'm stumped on a problem, and remembered my old adage: "ask DEV stuff, they know things". So, here's hoping someone can unstick me!

EDIT: Figured it out. Left the solution in the comments!

I'm trying to store a pointer to a function defined in C in a Rust struct, and call it from the Rust side. The rest of my FFI seems to be hooked up okay, but I'm getting a segfault when I make the actual function call. Debugging sessions have not proved useful.

I'll try to keep the context streamlined, but let me know if you need more to see what's going on.

First, here's the C externs, the function I'm pointing to, and the passage across the FFI boundary:

typedef struct scheduler scheduler_t;
typedef struct job job_t;
typedef void (*unit_to_unit_t)(void);

extern void run(job_t *, scheduler_t *, unit_to_unit_t);
extern job_t *every(uint32_t);
extern job_t *seconds(job_t *);

// Define a job
void job(void)
{
    //printf("Hello!  It is now %s\n", now());
    printf("Hello!");
}

run(seconds(every(8)), scheduler, job);
Enter fullscreen mode Exit fullscreen mode

Super exciting stuff. The commented out line calls a now()function that returns a char *, but I wanted to get anything working first. The every() function returns job_t *, and seconds() takes a job_t * and returns a job_t *, which is finally passed into run(). I have a typedef named unit_to_unit_t for the job function pointer - it's a function taking no arguments and returning nothing. The scheduler is also an FFI entity.

This is the definition of run() on the Rust side:

#[no_mangle]
pub unsafe extern "C" fn run(job: *mut Job, scheduler: *mut Scheduler, work: *const fn() -> ()) {
    let job = {
        assert!(!job.is_null());
        Box::from_raw(job)
    };
    let mut scheduler = {
        assert!(!scheduler.is_null());
        &mut *scheduler
    };

    let work: &fn() -> () = {
        assert!(!work.is_null());
        &*work
    };

    job.run(&mut scheduler, *work)
        .unwrap_or_else(|e| eprintln!("Error: {}", e));
}
Enter fullscreen mode Exit fullscreen mode

I'm passing the C function as the last argument here, and trying to cast it to work. This step doesn't complain, seems to go okay.

On the Rust side, when that job.run() method gets called, the following trait/struct is used to store the function pointer and call it:

pub trait Callable {
    /// Execute this callable
    fn call(&self) -> Option<bool>;
    /// Get the name of this callable
    fn name(&self) -> &str;
}

/// A named callable function taking no parameters and returning nothing.
#[derive(Debug)]
pub struct UnitToUnit {
    name: String,
    work: fn() -> (),
}

impl UnitToUnit {
    pub fn new(name: &str, work: fn() -> ()) -> Self {
        Self {
            name: name.into(),
            work,
        }
    }
}

impl Callable for UnitToUnit {
    fn call(&self) -> Option<bool> {
        // gets HERE just fine...
        (self.work)();
        None
    }
    fn name(&self) -> &str {
        &self.name
    }
}
Enter fullscreen mode Exit fullscreen mode

This all works fine with Rust function pointers. I determined that with the C version, we do get inside the call() implementation - the segfault happens when I use (self.work)();

I'm not sure if it's relevant, but the actual call is triggered here in C:

extern void run_pending(scheduler_t *);    

// Run some jobs
for (int i = 0; i < 100; i++)
{
    run_pending(scheduler);
    sleep(1);
}
Enter fullscreen mode Exit fullscreen mode

Which corresponds to this Rust interface fn:

#[no_mangle]
pub unsafe extern "C" fn run_pending(ptr: *mut Scheduler) {
    let scheduler = {
        assert!(!ptr.is_null());
        &mut *ptr
    };

    scheduler
        .run_pending()
        .unwrap_or_else(|e| eprintln!("Error: {}", e));
}
Enter fullscreen mode Exit fullscreen mode

Here's scheduler::run_pending():

    /// Run all jobs that are scheduled to run.  Does NOT run missed jobs!
    pub fn run_pending(&mut self) -> Result<()> {
        //let mut jobs_to_run: Vec<&Job> = self.jobs.iter().filter(|el| el.should_run()).collect();
        self.jobs.sort();
        let mut to_remove = Vec::new();
        for (idx, job) in self.jobs.iter_mut().enumerate() {
            if job.should_run() {
                let keep_going = job.execute()?;
                if !keep_going {
                    debug!("Cancelling job {}", job);
                    to_remove.push(idx);
                }
            }
        }
        // Remove any cancelled jobs
        to_remove.sort_unstable();
        to_remove.reverse();
        for &idx in &to_remove {
            self.jobs.remove(idx);
        }

        Ok(())
    }
Enter fullscreen mode Exit fullscreen mode

And finally, job::execute():

    /// Run this job and immediately reschedule it, returning true.  If job should cancel, return false.
    ///
    /// If the job's deadline has arrived already, the job does not run and returns false.
    ///
    /// If this execution causes the deadline to reach, it will run once and then return false.
    pub fn execute(&mut self) -> Result<bool> {
        if self.is_overdue(self.now()) {
            debug!("Deadline already reached, cancelling job {}", self);
            return Ok(false);
        }

        debug!("Running job {}", self);
        if self.job.is_none() {
            debug!("No work scheduled, moving on...");
            return Ok(true);
        }
        let _ = self.job.as_ref().unwrap().call(); // CALLED RIGHT HERE
        self.last_run = Some(self.now());
        self.schedule_next_run()?;

        if self.is_overdue(self.now()) {
            debug!("Execution went over deadline, cancelling job {}", self);
            return Ok(false);
        }

        Ok(true)
    }
Enter fullscreen mode Exit fullscreen mode

I added a comment where the actual call() happens.

This whole FFI shindig is just an experiment, I don't actually have a real need for this to work - but now that I got this far, I kinda want to know what I'm getting wrong! Thanks in advance.

Discussion (2)

Collapse
deciduously profile image
Ben Lovy Author • Edited on

To add a little more context, I was able to address the segfault by using the extern "C" fn() type:

#[no_mangle]
pub unsafe extern "C" fn run(job: *mut Job, scheduler: *mut Scheduler, work: extern "C" fn() -> ()) {
    let job = {
        assert!(!job.is_null());
        Box::from_raw(job)
    };
    let mut scheduler = {
        assert!(!scheduler.is_null());
        &mut *scheduler
    };

    job.run_extern(&mut scheduler, work)
        .unwrap_or_else(|e| eprintln!("Error: {}", e));
}
Enter fullscreen mode Exit fullscreen mode

I created a new ExternUnitToUnit struct that stores this type of function pointer and...it works, kind of. I changed the C function to this:

void job(void)
{
    exit(1);
}
Enter fullscreen mode Exit fullscreen mode

and when the job comes up to fire, the program indeed exits with a status code of 1. However, the printf statement doesn't work...hrm.

Collapse
deciduously profile image
Ben Lovy Author • Edited on

....and got there. Leaving this for posterity - STDOUT wasn't flushing. Gotta call it explicitly from C:

void job(void)
{
    //printf("Hello!  It is now %s\n", now());
    //exit(1);
    printf("Hello!");
    fflush(stdout);
}

Enter fullscreen mode Exit fullscreen mode