I have a platform I'm trying to setup to make a property based testing framework for roc.
I stubbed out the basics here
I would like to write some functions in rust that would be exposed to roc to get random strings, nums, etc. I gathered from reading some of the other platform examples i need to make roc_fx_<func>
defs and add them to an Effect.roc
file manually.
The issue im running into is any of the custom effects I added seem to cause the roc app to exit when called. If i stick to "pure" roc the app seems to run normally. I also copied some effects from basic cli to see if was how my functions were defined but seems to have the same issue.
I used roc glue <path to rust spec>
to generate the glue code but one difference I'm noticing between the glue it generated and the basic cli is how roc__mainForHost_xxxx
links are handled.
#[repr(C)]
#[derive(Debug)]
pub struct RocFunction_72 {
closure_data: Vec<u8>,
}
impl RocFunction_72 {
pub fn force_thunk(mut self) -> roc_std::RocResult<(), i32> {
extern "C" {
fn roc__mainForHost_0_caller(arg0: *const (), closure_data: *mut u8, output: *mut roc_std::RocResult<(), i32>);
}
let mut output = core::mem::MaybeUninit::uninit();
unsafe {
roc__mainForHost_0_caller(&(), self.closure_data.as_mut_ptr(), output.as_mut_ptr());
output.assume_init()
}
}
}
pub fn mainForHost() -> roc_std::RocResult<(), i32> {
extern "C" {
fn roc__mainForHost_1_exposed_generic(_: *mut roc_std::RocResult<(), i32>, );
}
let mut ret = core::mem::MaybeUninit::uninit();
unsafe {
roc__mainForHost_1_exposed_generic(ret.as_mut_ptr(), );
ret.assume_init()
}
}
but in basic-cli
i see
extern "C" {
#[link_name = "roc__mainForHost_1_exposed_generic"]
pub fn roc_main(output: *mut u8);
#[link_name = "roc__mainForHost_1_exposed_size"]
pub fn roc_main_size() -> i64;
#[link_name = "roc__mainForHost_0_caller"]
fn call_Fx(flags: *const u8, closure_data: *const u8, output: *mut u8);
#[allow(dead_code)]
#[link_name = "roc__mainForHost_0_size"]
fn size_Fx() -> i64;
#[link_name = "roc__mainForHost_0_result_size"]
fn size_Fx_result() -> i64;
}
Do i need to manually create some of these other extern funcs like roc_main_size
, call_Fx
, etc for roc to handle the effects correctly?
I couldn't find any platform making tutorial docs when poking around, do any exist? If not Id be happy to document some of the process to get the basics setup once I get something working
The docs don't exist cause we are working on a huge change (have been for a while). So documenting the process now isn't super useful.
As for the other functions there, you shouldn't need them. Glue should generate that correctly.
You will need the roc_fx_*
function which I don't think glue will generate
Probably would need to look at your platform specifically when I am on pc to give real advice
Ahh the doc stuff makes sense.
I tried to split up some of my rust code around so definitely possible I missed something during that
Yeah, first glance on phone looks fine.
The size function isn't needed cause the size of a task i32 is know at compile time
So glue can just generate all of that statically
And the caller function labeled call_Fx
is used in the force_thunk
function that glue generates`
@John Murray btw I'd love to include property based testing directly into expect
! Want to chat about it sometime?
Definitly! mostly using this as an excuse to make my own platform so would love to help out with pbt in expect
I'll try to take a look shortly, probably something simple. If you want to try and debug in the meantime, run with a debugger and print the stack trace when it crashes.
That will probably reveal a function with slightly off API or something
Oh, I bet this needs an explicit out param:
#[no_mangle]
pub extern "C" fn roc_fx_genStr() -> RocStr {
RocStr::from("abc")
}
out: *mut RocStr
so would it be something like
#[no_mangle]
pub extern "C" fn roc_fx_genStr(_out: *mut RocStr) -> RocStr {
RocStr::from("abc")
}
or would i need to set out to RocStr::from("abc")
?
Set out to that and return nothing
I updated to
#[no_mangle]
pub extern "C" fn roc_fx_genStr(out: *mut RocStr) {
unsafe {
out.write(RocStr::from("abc"));
}
}
with this roc program to test
app "simple"
packages { pf: "../platform/main.roc" }
imports [
pf.Task.{Task},
pf.Generator,
pf.Stdout,
]
provides [main] to pf
dbge = \x ->
dbg x
x
main : Task {} I32
main =
val <- Generator.genStr |> dbge |> Task.await
{} <- Stdout.line val |> dbge |> Task.await
Task.err 0
and the output I have is
roc dev examples/simple.roc
🔨 Rebuilding platform...
ENTERTING ROC
[examples/simple.roc:11] x = <opaque>
LEAVING ROC
RES:(RocOk(ManuallyDrop { value: () }))
the last line sometimes will be something like RES:(RocErr(ManuallyDrop { value: -1597949984 }))
were the number changes a lot.
Would that imply that maybe my manual bindings for things like alloc are wrong?
I ended up getting it to work but not using the generated glue in roc_app
and instead using the same rust_main
from basic cli
I wonder what glue has wrong
I'll have to dig into that in the future
Do you think you could file a bug with the failing version
Will do!
Not sure when I will get to it, maybe monday. About to travel, so not exactly sure my free time, but I do want to figure out what is going on here.
No rush! im unblocked for now so no worries.
The generated glue has this as main
mainForHost : Task {} I32
mainForHost = main
pub fn mainForHost() -> roc_std::RocResult<(), i32> {
extern "C" {
fn roc__mainForHost_1_exposed_generic(_: *mut roc_std::RocResult<(), i32>, );
}
let mut ret = core::mem::MaybeUninit::uninit();
unsafe {
roc__mainForHost_1_exposed_generic(ret.as_mut_ptr(), );
ret.assume_init()
}
}
so i think uninit is only making the space for a roc result and not the actual effect which needs to be resolved?
created https://github.com/roc-lang/roc/issues/6288
Another interesting place to look it basic-webserver.
It has a workaround to make glue generate the correct type for main
Here is the platform main which is modified so glue will generate correctly https://github.com/roc-lang/basic-webserver/blob/main/platform/main-glue.roc
This was a workaround Brendan provided, I am not sure how it works.
Haha....now I remember. Thanks for posting that.
The issue is that glue sees the task and assumes that the final value is being returned. So it doesnt generate code to call the task chain.
By writing it explicitly as a function that returns a closure, we are actually specific the underlying closure type that the task truly generates
That is why the workaround functions and what is need to make glue work here as well.
For generating this glue we would need a workaround like rocMainForHost : {} -> ({} -> Result {} I32)
or something like that for this example.
Or it might just be:
rocMainForHost : ({} -> Result {} I32)
Ahh that makes a lot of sense. Does the glue script not have enough introspection to know that task is opaque but it relies on effect?
Yeah, it just doesn't have special handling for this currently
Cause effect is a special type
Last updated: Jul 05 2025 at 12:14 UTC