mattwparas / steel

An embedded scheme interpreter in Rust
Apache License 2.0
1.11k stars 50 forks source link

Right way to use external global values #269

Open oosavu opened 1 week ago

oosavu commented 1 week ago

Hi! I am trying to use Steel in my project (system for musical livecoding). I want to be able to create custom objects in Steel virtual machine similar to register_types.rs example file. But these objects must have access to another global object in my enviroment. But i do not understand how to do it right! I am not able to declare MyGlobalCoreObject as a global static value for my rust program (and it is antipattern of course...), so how to access it?

Here is simplified example:


struct MyGlobalCoreObject {}

impl MyGlobalCoreObject {
    fn my_global_function(parameter: &str) {}
}

struct MyLittleObject {}

impl MyLittleObject {
    pub fn new(name: &str) -> Self {
        // Here i want to have access to the MyGlobalCoreObject and call my_global_function(name).
        // How to pass it to the MyLittleObject constructor?
        // do we need to pass the MyGlobalCoreObject here too? How to do it? 
        MyLittleObject {}
    }
}

pub fn main() {
    let mut vm = Engine::new();
    vm.register_type::<MyLittleObject>("MyLittleObject?");
    vm.register_fn("MyLittleObject", MyLittleObject::new);
    let global_object: MyGlobalCoreObject = {};
    vm.register_external_value("global_object", global_object)
        .unwrap();

    // how to implicitly pass the global_object here?
    vm
        .compile_and_run_raw_program(
            r#"
            (define my_little_object (MyLittleObject "foobarbaz"))
            "last-result"
        "#,
        );
}
mattwparas commented 1 week ago

The reason this is a bit silly mostly has to do with registered functions not having direct access to the steel runtime - they're more or less pure. There are ways to do that but I wouldn't consider those stable enough to rely upon.

So, alternatively, there are two ways to go about this. You can handle it in rust space, or you can handle it in user space within some steel boiler plate. Both have their trade offs, but here are both in the event you choose to go with one over the other:

Handling this in user space:

;; Define functions in steel code that pass the object through
;; manually, and just have the global object and functions be registered
;; in a module and use those.
(define (MyLittleObject name)
     (#%MyLittleObject #%global-object name))

This approach would require that all of your functions accept the global object as well as the additional parameters, and then you handle wrapping them up in steel code.

Alternatively, if you 100% know that your object is global and static, you could do something like this:


struct MyGlobalCoreObject {}

impl MyGlobalCoreObject {
    fn my_global_function(parameter: &str) {}
}

struct MyLittleObject {}

impl MyLittleObject {
    pub fn new(name: &str) -> Self {
        // Here i want to have access to the MyGlobalCoreObject and call my_global_function(name).
        // How to pass it to the MyLittleObject constructor?
        // do we need to pass the MyGlobalCoreObject here too? How to do it? 
        MyLittleObject {}
    }
}

pub fn main() {
    let mut vm = Engine::new();
    vm.register_type::<MyLittleObject>("MyLittleObject?");

    // You might not need to do this if your object itself is cheaply clonable - for example if under the
   //  hood it is already like Arc<T> or whatever, then you don't need to push it into a steel val first.
    let global_object: MyGlobalCoreObject = {}.into_steelval();
    vm.register_value("global_object", global_object);

   // Register a closure to move the value in directly, and then we can reference it here.
   // Alternatively, you could juse a lazy static or thread local as well in order to use pure functions.
    vm.register_fn("MyLittleObject",  move |name: SteelString| {
        let global = global_object.clone();
        let underlying = MyGlobalCoreObject::as_ref(&global).unwrap(); // You'll need to import the trait here
        // do whatever you'd like with your &underlying (or &mut - just use as_ref_mut)
    });

    // etc
}

Here is an example of the as_ref usage I mentioned above https://github.com/mattwparas/steel/blob/master/crates/steel-core/src/primitives/tcp.rs#L41

oosavu commented 1 week ago

mattwparas, thank you for the fast answer!!! (and thank you for this awesome project! :) ) Both approaches is suitable for me. But the first one looks better. Can you please clarify some points about it? What type should i pass in 'MyLittleObject::new' on a rust side? It must be 'SteelVal'? How to coerce it to MyGlobalCoreObject on a rust side? I also need to call 'as_ref' function?

mattwparas commented 1 week ago

You should be able to do something like this:

struct MyGlobalCoreObject {}
impl Custom for MyGlobalCoreObject {}

impl MyGlobalCoreObject {
    fn my_global_function(parameter: &str) {}
}

struct MyLittleObject {}

impl MyLittleObject {
    pub fn new(global: &MyGlobalCoreObject, name: &str) -> Self {
        /// etc
    }
}

The register_fn should do the proper type unwrapping for you

mattwparas commented 2 days ago

Is this resolved? Happy to answer any other questions if there are any