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.
I don't think so.... but I would have to test to verify
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....
Should be possible to make work, but not sure anyone has tried...so probably won't work
Would there be any difference if I settled for passing a tag that takes a single argument?
Like a command?
[ CallFn1 ArgType, CallFn2 ArgType, etc]
Or do you mean something different?
Yes, but for the return value, so like a Msg
in TEA
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.
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.
Why do you need the extra indirection?
Why not just have the host directly call update?
Because I don't know how to make the Rust glue generic over a
(and Msg
, since Msg
is defined in app-code)
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.
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
In your case, is Msg
truly opaque to the platform? It is always generated in roc and consumed in roc?
Yeah
Ok.
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.
Then have rust call update directly passing those two pointers
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?
There is definitely something about your architecture plan I am not understanding.
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).
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?
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
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.
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 Msg
s 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.
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
Indeed, but isn't that limited to synchronous effects?
not if your runtime is asynchronous
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?
oh sorry i didn't catch the WASM context. i don't really think i could help there
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
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
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
Yeah, so that function is callable from the host.
We expose a static function. The function takes all of the closure captures along with the standard arguments
The captures are the default zeroinitialized value you are seeing. Cause this case has not captures.
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
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
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.
I believe that is how boxes with at least in the generic caller functions, but would need to double check
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
When in a message that is based and sent to the host or when directly passed to the host?
And is the host only passing it back once or many times?
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