Taaitaaiger / jlrs

Julia bindings for Rust
MIT License
408 stars 21 forks source link

Passing Rust `String`s to Julia? #119

Closed har7an closed 5 months ago

har7an commented 5 months ago

Hello,

first up: thanks for creating and maintaining this project!

I have a library written in Rust, which I'd like to expose (in parts) to Julia. To that extent I created a toy project to get the hang of how this works. So far I can create opaque types on Rust, pass them to Julia and then call FFI functions with this type.

What I've been failing to do for about 10 hours now is returning a String from Rust to Julia. Whatever I try, either the type I want to return (e.g. JuliaString) doesn't impl the CCallReturn trait, or I have to provide some Target impl to a closure or anything like that whereupon I hit lifetime issues.

I'd consider myself a pretty experienced Rust developer, and I've got my fair share of experience with Julia, and most of what you explain in the memory, target or other module documentation of jlrs makes sense to me. But I cannot manage to fit the pieces together.

For reference, here's the code I'm playing with so far. I've had a semi-working version which allowed me to construct the instance in Rust and pass it over to Julia, and also let me call the is_apple function on it. But apparently I didn't commit it and now I can't reconstruct it. The code below is broken, consider it more of a goal I'd like to achieve.

use jlrs::data::managed::string::StringRet;
use jlrs::data::managed::value::typed::TypedValue;
use jlrs::data::managed::value::typed::TypedValueRet;
use jlrs::data::types::foreign_type::OpaqueType;
use jlrs::prelude::*;

#[repr(C)]
#[derive(Debug)]
pub enum MyOpaqueType {
    Apples = 0,
    Bananas,
    Oranges,
    Raspberries,
}

#[repr(C)]
#[derive(Debug, CCallReturn)]
pub struct ForJulia {
    pub inner: MyOpaqueType,
}

unsafe impl OpaqueType for ForJulia {}

impl ForJulia {
    pub fn new_apples() -> TypedValueRet<Self> {
        Self {
            inner: MyOpaqueType::Apples,
        }
    }

    pub fn is_apple(&self) -> u8 {
        matches!(self.inner, MyOpaqueType::Apples) as u8
    }

    pub fn name(&self) -> StringRet {
        let s = format!("{:?}", self.inner);
        unsafe { CCall::stackless_invoke(|unrooted| JuliaString::new(unrooted, s)) }
    }
}

fn main() {
    println!("Hello, world!");
}

jlrs::prelude::julia_module! {
    become myfoo_init;

    struct ForJulia;
    in ForJulia fn new_apples() -> TypedValueRet<ForJulia> as make_apple;
    in ForJulia fn is_apple(&self) -> u8 as is_apple;
    in ForJulia fn name(&self) -> JuliaString as name;
    fn main() -> () as say_hello;
}cat

And here's the Julia code to test it:

import Pkg;
Pkg.activate(".")
Pkg.instantiate()
Pkg.add("JlrsCore")

module CallMe
        using JlrsCore.Wrap

        @wrapmodule("./target/debug/libjlrs_demo.so", :myfoo_init)

        function __init__()
            @initjlrs
        end
end

CallMe.say_hello()
println(CallMe.make_apple)
apple = CallMe.make_apple()
println("Is apple: ", apple.is_apple())
println("Name: ", apple.name())
println(apple)

I'm grateful for any tip or hint you care to give me. Thanks in advance!

Taaitaaiger commented 5 months ago

I think the piece that's unclear is what exactly needs to be returned and why.

Let's look at new_apples first. It returns a TypedValueRet<Self>, which is an alias for Ref<'static, 'static, TypedValue<'static, 'static, Self>>. A TypedValue is a Value annotated with its type constructor (i.e. a Rust type that implements ConstructType). The difference between a TypedValue and Ref<TypedValue> is that that a TypedValue is guaranteed to be rooted while it's in scope, while a Ref might be unrooted. A Ref does provide a weaker guarantee: it's marked with the same lifetimes as if it had been rooted to prevent it from leaking out of jlrs's scope system.

Fundamentally, this entire system marks a pointer to some data managed by Julia in different ways to enforce some rules at compile time.

Unfortunately, leaking managed data from the scope system is exactly what we need to do here, hence the static lifetimes in TypedValueRet. Julia data has to be allocated, so we need to create a scope, but we need to leak this data from that scope to return it to Julia, Ref::leak exists for this purpose.

Putting this all together comes down to the following:

pub fn new_apples() -> TypedValueRet<Self> {
    unsafe {
        CCall::stackless_invoke(|unrooted| {
            let apples =Self {
                inner: MyOpaqueType::Apples,
            };

            TypedValue::new(unrooted, apples).leak()
        })
    }
}

The same goes for ForJulia::name, you need to leak the string to convert it to a StringRet.

har7an commented 5 months ago

Awesome, now it works! Thanks for the explanation, it seems that the "missing piece" in my case was the call to leak().

For reference, here's the working code:

Rust Code

```rust use jlrs::data::managed::string::StringRet; use jlrs::data::managed::value::typed::TypedValue; use jlrs::data::managed::value::typed::TypedValueRet; use jlrs::data::types::foreign_type::OpaqueType; use jlrs::prelude::*; #[repr(C)] #[derive(Debug)] pub enum MyOpaqueType { Apples = 0, Bananas, Oranges, Raspberries, } #[repr(C)] #[derive(Debug, CCallReturn)] pub struct ForJulia { pub inner: MyOpaqueType, } unsafe impl OpaqueType for ForJulia {} impl ForJulia { pub fn new_apples() -> TypedValueRet { unsafe { CCall::stackless_invoke(|unrooted| { TypedValue::new( unrooted, Self { inner: MyOpaqueType::Apples, }, ) .leak() }) } } pub fn is_apple(&self) -> u8 { matches!(self.inner, MyOpaqueType::Apples) as u8 } pub fn name(&self) -> StringRet { let s = format!("{:?}", self.inner); unsafe { CCall::stackless_invoke(|unrooted| JuliaString::new(unrooted, s).leak()) } } } fn main() { println!("Hello, world!"); } jlrs::prelude::julia_module! { become myfoo_init; struct ForJulia; in ForJulia fn new_apples() -> TypedValueRet as make_apple; in ForJulia fn is_apple(&self) -> u8 as is_apple; in ForJulia fn name(&self) -> StringRet as name; fn main() -> () as say_hello; } ```

Julia Code

```julia import Pkg; Pkg.activate(".") Pkg.instantiate() Pkg.add("JlrsCore") module CallMe using JlrsCore.Wrap @wrapmodule("./target/debug/libjlrs_demo.so", :myfoo_init) function __init__() @initjlrs end end CallMe.say_hello() apple = CallMe.make_apple() println("Is apple: ", CallMe.is_apple(apple)) println("Name: ", CallMe.name(apple)) println(apple) ```

Thank you very much!