Stream: beginners

Topic: Send a Roc function to the host and back


view this post on Zulip sasiki (May 18 2025 at 15:19):

Is it possible to box an arbitrary Roc function, pass it to a Rust host, and have the host pass it back to Roc, then unbox it and call it in Roc?

The purpose is to be able to have a kind of callback from async host functions. I don't want the Rust host to have to know or care about what arguments the function takes or what it returns, that'd strictly be a Roc concern.

view this post on Zulip Brendan Hansknecht (May 18 2025 at 15:41):

I don't think so.... but I would have to test to verify

view this post on Zulip Brendan Hansknecht (May 18 2025 at 15:42):

The issue is that we handle every function that is passed to the host in a special way to make it callable by the host and I don't think we actually support proper boxing of functions today....

view this post on Zulip Brendan Hansknecht (May 18 2025 at 15:42):

Should be possible to make work, but not sure anyone has tried...so probably won't work

view this post on Zulip sasiki (May 18 2025 at 15:43):

Would there be any difference if I settled for passing a tag that takes a single argument?

view this post on Zulip Brendan Hansknecht (May 18 2025 at 15:46):

Like a command?

[ CallFn1 ArgType, CallFn2 ArgType, etc]

view this post on Zulip Brendan Hansknecht (May 18 2025 at 15:46):

Or do you mean something different?

view this post on Zulip sasiki (May 18 2025 at 15:53):

Yes, but for the return value, so like a Msg in TEA

view this post on Zulip sasiki (May 18 2025 at 15:55):

The Rust host function would take a (a -> Msg), perform some effect, and return a, (a -> Msg) so that Roc could put those two together to produce a Msg and trigger that. I'm not writing exactly TEA, but that's a more clear example.

view this post on Zulip sasiki (May 18 2025 at 15:57):

Sorry, not "return" as it's an async function. It would call some Roc host function with a, (a -> Msg). In the TEA example that'd be e.g. call_update(a, a -> Msg) which would put them together and call the app function update(Msg). I guess TEA has a Model too, but that's not relevant to the example.

view this post on Zulip Brendan Hansknecht (May 18 2025 at 16:00):

Why do you need the extra indirection?

Why not just have the host directly call update?

view this post on Zulip sasiki (May 18 2025 at 16:04):

Because I don't know how to make the Rust glue generic over a

view this post on Zulip sasiki (May 18 2025 at 16:07):

(and Msg, since Msg is defined in app-code)

view this post on Zulip sasiki (May 18 2025 at 16:12):

So I just thought boxing the function and its argument would make it easier. That'd allow me to skip wrangling Rust glue (which I don't know much about) and just pass _anything_ back and forth.

view this post on Zulip Brendan Hansknecht (May 18 2025 at 16:14):

I would just avoid the closure, but otherwise, this should be doable without too much hassle.

For things like basic webserver, we already make rust take an opaque model. It gets the model from init and passes it to update repeatedly

view this post on Zulip Brendan Hansknecht (May 18 2025 at 16:14):

In your case, is Msg truly opaque to the platform? It is always generated in roc and consumed in roc?

view this post on Zulip sasiki (May 18 2025 at 16:15):

Yeah

view this post on Zulip Brendan Hansknecht (May 18 2025 at 16:15):

Ok.

view this post on Zulip Brendan Hansknecht (May 18 2025 at 16:15):

Then just like model is boxed and rust only handles a pointer, you could box msg and have rust only handle a pointer to it.

view this post on Zulip Brendan Hansknecht (May 18 2025 at 16:15):

Then have rust call update directly passing those two pointers

view this post on Zulip sasiki (May 18 2025 at 16:36):

I'm not following completely... When you say "you could box msg" do you mean boxing the constructor (a -> Msg)? I can't construct the Msg on the Roc-side without first getting whatever value it should contain from the Rust-side. Let's say I have this Roc host function:

http_get! : Url, Box (Result Response HttpErr -> Msg) => {}
http_get! = |url, msg_constructor| ...

The Rust implementation of that would make the HTTP request, build the RocResult and then it's stuck between two worlds: it has the boxed constructor and it has a, but it can't put them together (that would require specific glue for each type of of effect to say what the return value is).

That's why I thought I'd keep it generic and just box it all up, send it over to Roc, and let Roc apply it.

I feel like I'm missing something?

view this post on Zulip Brendan Hansknecht (May 18 2025 at 19:23):

There is definitely something about your architecture plan I am not understanding.

view this post on Zulip Brendan Hansknecht (May 18 2025 at 19:28):

Why isn't http_get! simply

http_get! : Url => Result Response HttpErr

Or if you explicitly want a command and message system, I would expect something like this:

update: Box model, Msg -> (Box model, List Cmd)

Where Msg is a wrapper around any possible output the platform could alert the app of (like recieving an http response) and Cmd is anything the app can request the platform do (like make an http request).

view this post on Zulip sasiki (May 18 2025 at 20:55):

Why isn't http_get! simply

Because I do want some kind of command and message system like you showed.

Where Msg is a wrapper around any possible output the platform could alert the app of

I'm stuck because Msg is app-defined, so the platform doesn't know how to construct one. Here's roughly what you'd write in Elm:

# Possible Msgs
Msg : [
    MyHttpRequest (Result Response HttpErr),
    ...
]

# init constructs a Cmd and that will cause a MyHttpRequest message to be fired
init : _ -> (Model, Cmd Msg)
init = |_| ({}, Http.request("https://www.example.com", MyHttpRequest))

I then execute that Cmd on the Rust-side of the platform, and end up with a result that I want to pass back in the form of a Msg to the app-code. How do I construct that MyHttpRequest (Result Response HttpErr) in Rust?

My idea was to box everything. I box the message constructor when I pass it over to the Rust-side, I box the result of the effect on the Rust-side, and I pass both of those boxed things back to the Roc-side of the platform. Then Roc can unbox them and apply the function to the resulting argument. I just have to be responsible and make sure the type of the constructor and the argument lines up. If they do, Rust shouldn't have to know what Msg it was asked to produce.

This should result in very little Rust glue code per effect.

Do you think this is possible?

view this post on Zulip Anthony Bullard (May 18 2025 at 21:14):

i've been working on something similar for awhile. your overall design is pretty on point but orthogonal to effectful-ness in Roc. i would instead model the interface to these effects using a generic mechanism like a Cmd type that the platform controls . just like Elm's runtime

view this post on Zulip Anthony Bullard (May 18 2025 at 21:15):

but you are basically opting into a turn-based or actor-style programming model(nothing wrong with that - that's what i'm doing - but it's a different model than all other Roc platforms.

view this post on Zulip sasiki (May 19 2025 at 09:02):

IIUC I would still run into the same issue with Cmd, no? IIUC a Cmd is instruction to the runtime to perform an effect and to send a Msg back with the result of the effect as the Msgs payload. The user passes in a constructor for the Msg that they want to receive back.

The Rust-side of the platform would have to be able to apply the constructor given by the user to the result of the effect. That means it would have to know the types involved, which means you would have to write glue code for every effect that you expose (please correct me if I'm wrong, I don't know much about Roc/Rust interop).

My idea was to skip the glue code by boxing the constructor and its argument and doing the application in Roc. Then you'd have minimal glue. I just don't know enough to figure out how to box a function, pass it to Rust, pass it back to Roc, unbox it, and apply it to a value.

view this post on Zulip Anthony Bullard (May 19 2025 at 10:31):

It definitely wouldn't need to know the types, if the Roc platform code handles calling the hosted functions. so you would do a when on the Cmd type, calling the appropriate hosted function for each and then calling the passed in lambda in the Roc code with the value returned from the hosted function and then return that result

view this post on Zulip sasiki (May 19 2025 at 11:00):

Indeed, but isn't that limited to synchronous effects?

view this post on Zulip Anthony Bullard (May 19 2025 at 11:19):

not if your runtime is asynchronous

view this post on Zulip sasiki (May 19 2025 at 17:20):

I'm in the browser with WASM, so I believe I'm quite restricted when it comes to async. I can't block at all, so to make an HTTP request for example I'd need to use wasm_bindgen_futures::spawn_local which returns unit immediately and runs the async block you give it in the background. To get the result of the HTTP request I'd need to call another function within the async block (i.e. a callback). Here's an example from Yew:

wasm_bindgen_futures::spawn_local(async move {
   let fetched_videos: Vec<Video> = Request::get("https://yew.rs/tutorial/data.json")
       .send()
       .await
       .unwrap()
       .json()
       .await
       .unwrap();
   videos.set(fetched_videos);
});

So IIUC the Rust host function wouldn't be able to return the HTTP result directly. It would have to call another Roc host function with the result, but it would also have to pass along the Msg constructor so that the Roc host function can apply the constructor to the result of the HTTP request. Am I making sense?

view this post on Zulip Anthony Bullard (May 19 2025 at 17:38):

oh sorry i didn't catch the WASM context. i don't really think i could help there

view this post on Zulip Nathan Kamenchu (May 24 2025 at 13:13):

I have had this need as well of passing a function to a rust host and calling it again

Here's a minimal repo where I attempt to isolate the simplest possible example of this: https://github.com/kamenchunathan/platform_tests

view this post on Zulip Nathan Kamenchu (May 24 2025 at 14:53):

Maybe there's a way around it but I do not see a way to design around these specific limitations without being able to box functions or type constructors which I assume to be the same implementation-wise

view this post on Zulip Nathan Kamenchu (May 24 2025 at 14:56):

This platform is a simple platform that isolates passing a function through the boundary
Looking at the llvm IR for the compilation of the following user application code,

app [
    store,
    use!,
    Store,
] { pf: platform "../platform/main.roc" }

import pf.Effects exposing [print!]

Store : I32 -> I32

store : Store
store = |_| 42069

use! : Store => {}
use! = |fs|
    print! (Inspect.to_str (fs 89787))
    {}
define void @roc__store_for_host_1_exposed_generic(ptr %0, i32 %1) !dbg !9 {
entry:
  %call = call fastcc ptr @"_store_for_host!_99e2ebbd98e8a2a4c7ed9bd71d205d9f7b5d7e7a9ddb68dab65f2ad1c2198b"(i32 %1), !dbg !10
  store ptr %call, ptr %0, align 8, !dbg !10
  ret void, !dbg !10
}

define internal fastcc ptr @"_store_for_host!_99e2ebbd98e8a2a4c7ed9bd71d205d9f7b5d7e7a9ddb68dab65f2ad1c2198b"(i32 %"4") !dbg !3 {
entry:
  %call = call fastcc ptr @Box_box_f03bf86f79d121cbfd774dec4a65912e99f5f17c33852bbc45e81916e62b53b({} zeroinitializer), !dbg !7
  ret ptr %call, !dbg !7
}

roc seems to box a default zeroinitialized value and then create a separate function

define void @roc__use_for_host_0_caller(ptr %0, ptr %1, ptr %2) {
entry:
  %load_param = load i32, ptr %0, align 4, !dbg !51
  %call = call fastcc i32 @_10_4e123451c288c52798d3df0fc84811d2d957f324242982575c70dfd6d338df(i32 %load_param, ptr %1), !dbg !51
  store i32 %call, ptr %2, align 4, !dbg !51
  ret void, !dbg !51
}

If I understood @Brendan Hansknecht what you were saying is that this function is meant to be callable by the host, but wouldn't this require compile time linkage?

I'm also not too experienced in reading llvm IR so if I've made any wrong assumptions I'd appreciate being corrected

view this post on Zulip Brendan Hansknecht (May 24 2025 at 16:56):

Yeah, so that function is callable from the host.

view this post on Zulip Brendan Hansknecht (May 24 2025 at 16:56):

We expose a static function. The function takes all of the closure captures along with the standard arguments

view this post on Zulip Brendan Hansknecht (May 24 2025 at 16:57):

The captures are the default zeroinitialized value you are seeing. Cause this case has not captures.

view this post on Zulip Nathan Kamenchu (May 26 2025 at 06:24):

Updated the code in the repo to have the following control flow
the platform passes a callback to the rust host, the rust host calls the callback with the appropriate arguments and gets the Msg type from the callbacks return, the calls the roc platform code again with this Msg type.

Here's the relevant code

#[repr(C)]
pub struct Captures {
    _data: (),
    _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}

#[repr(C)]
pub struct Msg {
    _data: (),
    _marker: core::marker::PhantomData<(*mut u8, core::marker::PhantomPinned)>,
}

pub fn call_roc_setup_callback() -> *const Captures {
    extern "C" {
        #[link_name = "roc__setup_callback_for_host_1_exposed_generic"]
        fn caller(_: *mut Captures, _: i32);

        #[link_name = "roc__setup_callback_for_host_1_exposed_size"]
        fn size() -> usize;
    }

    unsafe {
        let captures = roc_alloc(size(), 0) as *mut Captures;
        caller(captures, 0);
        captures as *const Captures
    }
}

pub fn call_roc_callback(captures: *const Captures) -> *const Msg {
    extern "C" {
        #[link_name = "roc__setup_callback_for_host_0_caller"]
        fn caller(_: *const i32, _: *const Captures, _: *mut Msg);

        #[link_name = "roc__setup_callback_for_host_0_result_size"]
        fn size() -> isize;
    }

    unsafe {
        let msg_size = size();
        println!("msg size: {msg_size}");
        let ret = if msg_size == 0 {
            std::ptr::NonNull::dangling().as_ptr()
        } else {
            roc_alloc(size() as usize, 0) as *mut Msg
        };
        caller(&0, captures, ret);
        println!("wow");
        ret as *const Msg
    }
}

pub fn call_roc_handle_callback(msg: *const Msg) {
    extern "C" {
        #[link_name = "roc__handle_callback_for_host_1_exposed"]
        fn caller(_: *const Msg);
    }

    unsafe { caller(msg) };
}

#[no_mangle]
pub extern "C" fn rust_main() -> i32 {
    let captures = roc::call_roc_setup_callback();
    let msg = roc::call_roc_callback(captures);
    roc::call_roc_handle_callback(msg);

    0
}
platform "wow" requires { Msg } {
        on_event : Event -> Msg,
        handle! : Msg => {},
    }
    exposes [Effects, Event]
    packages {
    }
    imports []
    provides [
        setup_callback_for_host!,
        handle_callback_for_host!,
    ]

import Event exposing [Event]

setup_callback_for_host! : I32 => (Event -> Box Msg)
setup_callback_for_host! = |_|
    wrapped = |e| Box.box (on_event e)
    wrapped

handle_callback_for_host! : Box Msg => {}
handle_callback_for_host! = |boxed_msg|
    msg = Box.unbox boxed_msg
    handle! msg

view this post on Zulip Nathan Kamenchu (May 26 2025 at 06:31):

This code however has issues in passing the Boxed result to and fro the host and platform,
the roc__setup_callback_for_host_0_caller seems to only be able to take a pointer to the result, what's the recommended way of creating a RocBox<T> in host code

view this post on Zulip Brendan Hansknecht (May 26 2025 at 14:54):

I think you can make the function take a *mut *mut void. Then call it while passing in a reference to a *mut void on the stack as a local variable. That should get you the boxes pointer. Sine rust is just passing the box back into roc, no need to have the host even know it is a box. Just passing that into the other roc function.

view this post on Zulip Brendan Hansknecht (May 26 2025 at 14:56):

I believe that is how boxes with at least in the generic caller functions, but would need to double check

view this post on Zulip Nathan Kamenchu (May 26 2025 at 16:01):

This works for types that do not allocate, but roc string, and lists seem to go to the default value when they are passed back to the roc platform code from the host
What could be the cause of this

view this post on Zulip Brendan Hansknecht (May 26 2025 at 16:23):

When in a message that is based and sent to the host or when directly passed to the host?

view this post on Zulip Brendan Hansknecht (May 26 2025 at 16:23):

And is the host only passing it back once or many times?

view this post on Zulip Nathan Kamenchu (May 27 2025 at 09:46):

Was actually an FFI issue where I didn't dereference the pointer to the box hence it was passing in garbage memory to the callback.
I don't know how I didn't notice the value types constantly changin


Last updated: Jul 06 2025 at 12:14 UTC