Taaitaaiger/rustfft-jl

Guide for porting other Rust libraries

Opened this issue · 22 comments

VarLad commented

I'd like to make similar Julia bindings for 2 other Rust libraries, neither of which have a C API
Would it be possible to somewhat make an efficient guide which one can use to port Rust libraries to Julia using JLRS?

I'm ready to help with or write said guide with some guidance.

This is something that I want to write about in the planned series of jlrs tutorials, but I have to admit I haven't done much more than roughly planning an outline and I'd consider this a reasonably advanced topic that requires some familiarity with jlrs as a whole. For now, most information is available in this crate and the documentation for the julia_module macro.

I'd be happy to answer any questions you have to help you write those bindings, I think the approach I've taken for RustFFT can be summarized as follows:

  • Create bindings for types defined in Julia that you need direct access to with JlrsCore.Reflect.
  • Expose the Rust types to Julia by implementing OpaqueType for wrapper types (to account for Rust's orphan rules).
  • Either write unsafe extern "C" functions or methods with compatible signatures that call some function from the targeted library
VarLad commented

@Taaitaaiger Can Rust's

struct Point<T> {
    x: T,
    y: T,
}

be converted to Julia's

struct Point{T}
    x::T
    y::T
end

Similarly for functions, can those be translated similarly?

Maybe I'm wrong here, but I'd think porting Rust libraries directly to Julia would be easier than writing a C API since Julia has a lot of the same structures as Rust.

By the way, I'm trying to port https://github.com/rust-windowing/winit/blob/master/src/event_loop.rs (the entire crate, but starting with this file for now)
Not sure how to start with porting this.
For example, stuff like

pub struct EventLoopWindowTarget<T: 'static> {
    pub(crate) p: platform_impl::EventLoopWindowTarget<T>,
    pub(crate) _marker: PhantomData<*mut ()>, // Not Send nor Sync
}

is something I don't think I can translate directly, so should I try getting this as an opaque type?

Other than that, any general suggestions on how should I start?

If Point is repr(C), yes. You can see that the bindings generated with JlrsCore.Reflect match your Rust implementation:

julia> using JlrsCore.Reflect

julia> struct Point{T}
           x::T
           y::T
       end

julia> reflect([Point])
#[repr(C)]
#[derive(Clone, Debug, Unbox, ValidLayout, Typecheck, ValidField, ConstructType, CCallArg, CCallReturn)]
#[jlrs(julia_type = "Main.Point")]
pub struct Point<T> {
    pub x: T,
    pub y: T,
}

I've spent some time reading the docs, and the thing that's quite challenging here is that the event loop is not thread-safe. As such, it can't implement OpaqueType because that trait requires Send and Sync.

If your goal is to be able to run the event loop in Rust and be able to send events from Julia, I think the best approach is to write a function that does the following:

  • create a channel
  • spawn a new thread which creates an event loop and an EventLoopProxy for that loop
  • send the proxy to the original thread with the channel, the proxy is thread-safe if T is

It should be possible to write a wrapper struct for EventLoopProxy that implements OpaqueType and has a method to call the proxy's send_event method. This wrapper should also contain the JoinHandle returned by thread::spawn so you can join the thread when the final proxy is dropped.

VarLad commented

@Taaitaaiger As an alternative, how about wrapping the glazier crate instead?
Glazier seems much simpler to wrap than Winit. And as of the latest commit, the app handles are thread-safe too
I'm thinking of starting with this file: https://github.com/linebender/glazier/blob/main/src/application.rs

By the way, if I go with glazier, I won't have to write bindings for any of the stuff in https://github.com/linebender/glazier/tree/main/src/backend right?

Thanks for the help! :)

I think, but I haven't taken a good look at glazier, that the challenges are similar. In both cases you'd want to run the event loop on another thread to avoid blocking Julia.

VarLad commented

@Taaitaaiger My apologies for the delay!
I hope you can give some advice here. I've started by porting the https://github.com/linebender/glazier/blob/main/src/application.rs file as of now.

More specifically, the following code:

use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};

use crate::backend::application as backend;
use crate::clipboard::Clipboard;
use crate::error::Error;
use crate::util;

#[derive(Clone)]
pub struct Application {
    pub(crate) backend_app: backend::Application,
    state: Rc<RefCell<State>>,
}

/// Platform-independent `Application` state.
struct State {
    running: bool,
}

/// Used to ensure only one Application instance is ever created.
static APPLICATION_CREATED: AtomicBool = AtomicBool::new(false);

thread_local! {
    /// A reference object to the current `Application`, if any.
    static GLOBAL_APP: RefCell<Option<Application>> = RefCell::new(None);
}

impl Application {
    /// Create a new `Application`.
    ///
    /// # Errors
    ///
    /// Errors if an `Application` has already been created.
    ///
    /// This may change in the future. See [druid#771] for discussion.
    ///
    /// [druid#771]: https://github.com/linebender/druid/issues/771
    pub fn new() -> Result<Application, Error> {
        APPLICATION_CREATED
            .compare_exchange(false, true, Ordering::AcqRel, Ordering::Acquire)
            .map_err(|_| Error::ApplicationAlreadyExists)?;
        util::claim_main_thread();
        let backend_app = backend::Application::new()?;
        let state = Rc::new(RefCell::new(State { running: false }));
        let app = Application { backend_app, state };
        GLOBAL_APP.with(|global_app| {
            *global_app.borrow_mut() = Some(app.clone());
        });
        Ok(app)
    }

I was trying to wrap the new function first.
A few things I'm not sure about is:

  1. How should I wrap the Application struct as an OpaqueType?
    Does
use glazier::application::{
    backend::Application as ApplicationImpl,
};

struct Application(ApplicationImpl, Rc<RefCell<State>>)

unsafe impl OpaqueType for Application {}

make sense?

  1. How do I deal with Result<Application, Error>?

  2. What about the other variables defined? Also, how do I port the State function? JlrsCore.Reflect.reflect([Bool]) gives an error, I think that means that Bool is implemented differently in Julia and Rust.

VarLad commented

@Taaitaaiger I also don't understand when to use TypedValueRet

Here's a broken MWE I could use your help on:

use std::sync::Async;
use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};

use jlrs::{
    ccall::AsyncCallback,
    convert::compatible::{Compatible, CompatibleCast},
    data::{
        layout::valid_layout::ValidField,
        managed::{
            array::{TypedRankedArray, TypedRankedArrayUnbound},
            rust_result::{RustResult, RustResultRet},
            value::typed::{TypedValue, TypedValueRet, TypedValueUnbound},
        },
        types::{construct_type::ConstructType, foreign_type::OpaqueType},
    },
    error::JlrsError,
    prelude::*,
};

use glazier::application::{
    Application as ApplicationImpl,
    backend::application::Application as BackendApplicationImpl,
};

struct State {
    running: bool
}

pub type RcRefCellState = Rc<RefCell<State>>;

pub struct Application(BackendApplicationImpl, RcRefCellState);

unsafe impl OpaqueType for RcRefCellState {}
unsafe impl OpaqueType for Application {}

impl Application
where
    Self: OpaqueType,
{
    #[inline]
    pub fn new() -> JlrsResult<Self> {
        unsafe {
            Ccall::stackless_invoke(|unrooted| {
                let state = Rc::new(RefCell::new(State { running: false }));
                let application = Application(ApplicationImpl::new()?, state);
                TypedValue::new(unrooted, application)
            })
        }
    }
}
VarLad commented

@Taaitaaiger I see that you have replaced OpaqueTypes with ParametricTypes in the latest commit

Expose the Rust types to Julia by implementing OpaqueType for wrapper types (to account for Rust's orphan rules).

Any advice now? ^^;

I expect it's failing because you need to leak the returned value: TypedValue::new(unrooted, application).leak()

You can still use OpaqueType because neither type has any type parameters.

How do I deal with Result<Application, Error>?

You can convert it to a boxed JlrsError and return it as a JlrsResult to throw it as a JlrsCore.JlrsError, or convert the Error to ValueRet and return it as Result<Application, ValueRet> to throw that value as an exception.

What about the other variables defined? Also, how do I port the State function? JlrsCore.Reflect.reflect([Bool]) gives an error, I think that means that Bool is implemented differently in Julia and Rust.

Bool is offered by jlrs itself (see: jlrs::data::layout::Bool), that it fails with an error is a bug though. That said, opaque types can only be accessed from Rust code. If you want to access a field of a type, you have to export a function defined in Rust to access it. If you want to be able to access it from Julia code, define it in Julia and generate bindings for that struct.

VarLad commented

Thanks!
Going with

use glazier::Application as ApplicationImpl;

pub struct Application(ApplicationImpl);

unsafe impl OpaqueType for Application {}

impl Application where Self: OpaqueType {
    #[inline]
    pub fn new() -> TypedValueRet<Application> {
        unsafe {
            Ccall::stackless_invoke(|unrooted| {
                let application = Application(ApplicationImpl::new()?);
                TypedValue::new(unrooted, application).leak()
            })
        }
    }
}

I'm getting the error,

error[E0277]: `Rc<RefCell<glazier::application::State>>` cannot be sent between threads safely
   --> src/application.rs:65:28
    |
65  | unsafe impl OpaqueType for Application {}
    |                            ^^^^^^^^^^^ `Rc<RefCell<glazier::application::State>>` cannot be sent between threads safely

Any idea how I can solve this?

VarLad commented

@Taaitaaiger By the way, can I ask, when is it a good idea to use Ccall::stackless_invoke and when not?
It appears that you've used Ccall.stackless_invoke in some places and not others

Ah right, Application has to implement Send and Sync to account for the fact that it might be used from different threads in Julia and that it is dropped by a finalizer which can also happen on another thread than the one that created Application. You will need to use Arc instead of Rc, and protect the RefCell with a GC-safe mutex or RwLock (see here for more info)

stackless_invoke has a pretty bad name. It creates a scope without a dynamic stack (CCall::scope) or creating a local frame (CCall::local_scope), but only provides an instance of the Unrooted target. It's used when nothing needs to be rooted to avoid the overhead associated with creating those stacks and frames.

VarLad commented

@Taaitaaiger Would that mean that I've to reimplement the Application struct?
Originally its implemented as:

use std::rc::Rc;
use crate::backend::application as backend;

#[derive(Clone)]
pub struct Application {
    pub(crate) backend_app: backend::Application,
    state: Rc<RefCell<State>>,
}

so will I have to reimplement (in my library) as:

use std::sync::Arc;
use glazier::backend::application as backend;

#[derive(Clone)]
pub struct Application(backend::Application, Arc<GcSafeRwLock<RefCell<State>>>)

Won't changing how Application is defined be an issue...?

or did you mean something else and I totally misunderstood? 😓

Yes, you'll need to change Application to be able to expose it to Julia, and adjust other code to account for those changes.

Overall, I think exposing Application to Julia is probably the wrong approach because it might not be possible to expose it in a thread-safe way. I think it's better to run the application on a separate thread, and expose AppHandle which I expect will be thread-safe. That will prevent you from calling directly into Julia from the application thread (NB: this might change in the future), but if you want to be able to call into Julia from Application things quickly get more an more complex.

VarLad commented

That does make more sense, thanks for the advice!

(Edit: Deleted previous question)

You would've notice as the input of some functions above being
Option<Box<dyn AppHandler>>

where AppHandler is defined as

pub trait AppHandler {
    /// Called when a menu item is selected.
    #[allow(unused_variables)]
    fn command(&mut self, id: u32) {}
}

Not sure but, does this mean I've to wrap AppHandler as an OpaqueType too?

VarLad commented

Edit:

AppHandle's run_on_main is defined as:

impl AppHandle {
    pub fn run_on_main<F>(&self, callback: F)
    where
        F: FnOnce(Option<&mut dyn AppHandler>) + Send + 'static,
    {
        self.0.run_on_main(callback);
    }
}

If I'm getting this correctly, the type of callback is basically a Rust function (more specifically a closure) right?
Ideally I'd want to provide a Julia function/closure here. Does JLRS provide a way to deal with cases like this?

VarLad commented

@Taaitaaiger I think the only way to do this is to convert Option<&mut dyn AppHandler> it to an opaque pointer somehow, and have a Rust function which returns this opaque pointer?

Sorry for the late reply, I'm a bit busy atm.

An opaque type can't contain any references, one of the requirements to implement the traits that let you export a type to Julia is that Self: 'static. The reason for this is that there are no guarantees about when data is dropped and in what order, Julia is also completely unaware of the existence of lifetimes.

Functions that take &mut self can be exposed to Julia with the in <Type> fn func(&mut self, ...) -> <RetType> syntax. You can't provide a closure from Julia, you have to write that in your Rust code. Basically, for each operation that requires a custom F, you need to implement a separate function on the opaque type and export it.

VarLad commented

@Taaitaaiger Sorry to be disturbing you when you're busy, but I wonder if you're open to some discussion on how to handle closures (in some capacity) from Julia.

I tried creating a MVP with the above library,

use glazier::{
    AppHandle as AppHandleImpl,
    Application
};

#[no_mangle]
pub extern "C" fn run_on_main_api(a: &AppHandleImpl, f: extern "C" fn()) {
  AppHandleImpl::run_on_main(a, move |_| f());
}

#[no_mangle]
pub extern "C" fn new_func() {
    println!("Hey! This might work!");
}

fn main() {
    let app = Application::new().expect("Failed to create application");
    let handle = app.get_handle().expect("Failed to get application handle");
    run_on_main_api(&handle, new_func);
    app.run(None);
}

This function:

#[no_mangle]
pub extern "C" fn new_func() {
    println!("Hey! This might work!");
}

can of course be provided from Julia like,

function new_func() {
    println("Hey! This might work!")
}

c_new_func = @cfunction(new_func, Cvoid, ())

I think if we can think of a way to wrap the Option<&mut dyn AppHandler> thingy, me might be able to do this.

Just a dumb example though, I'd love to hear your opinion whenever you're free to do so!

VarLad commented

To be a bit clearer, if we can wrap Option<&mut dyn AppHandler> somehow (lets call that wrapper struct X)
Then we can probably write

#[no_mangle]
pub extern "C" fn run_on_main_api_with_apphandler(a: &AppHandleImpl, f: extern "C" fn(f: *mut X)) {
  AppHandleImpl::run_on_main(a, f);
}