Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Question: Is it possible to have an indexer return a callable closure back to Rhai? #951

Closed
makspll opened this issue Jan 12, 2025 · 11 comments · Fixed by #952
Closed

Question: Is it possible to have an indexer return a callable closure back to Rhai? #951

makspll opened this issue Jan 12, 2025 · 11 comments · Fixed by #952

Comments

@makspll
Copy link

makspll commented Jan 12, 2025

I am trying to attach an indexer function which performs a dynamic lookup on a function registry (basically bunch of Arc<fn>'s with a specifc signature).

I want to be able to:

let my_dynamic_type = ...
my_dynamic_type.dynamic_function("asd")
my_dynamic_type.dynamic_function2("asd")

given:

#[derive(Clone, Debug, Copy, PartialEq)]
pub struct RhaiStaticReflectReference(pub TypeId);

impl CustomType for RhaiStaticReflectReference {
    fn build(mut builder: rhai::TypeBuilder<Self>) {
        builder
            .with_name(std::any::type_name::<RhaiStaticReflectReference>())
            .with_indexer_get(|self_: &mut Self, index: Dynamic| {
                let world = ThreadWorldContainer.get_world();
                let type_id = self_.0;
                let key: ScriptValue = ScriptValue::from_dynamic(index)?;

                let key = match key.as_string() {
                    // i.e. `dynamic_function` is looked up
                    Ok(name) => match world.lookup_function([type_id], name) {
                        Ok(func) => return ScriptValue::Function(func).into_dynamic(),
                        Err(key) => ScriptValue::String(key),
                    },
                    Err(key) => key,
                };

                let world = ThreadWorldContainer.get_world();
                Err::<_, Box<EvalAltResult>>(
                    InteropError::missing_function(type_id, key.display_with_world(world.clone()))
                        .into(),
                )
            });
    }
}

I know that Dynamic supports FnPtr which you can create via a ScriptFuncDef, however that seems to only be appropriate for actual "script defined" functions

@schungx
Copy link
Collaborator

schungx commented Jan 13, 2025

Does FnPtr::new work for you? You can just name any function.

@makspll
Copy link
Author

makspll commented Jan 13, 2025

Not in this case since the functions are not registered into rhai. Ideally i wanted not to have to do that, however trying to register them also seems out of possibility, since i dont have the compile time information necessary (arity is dynamic as well)

@schungx
Copy link
Collaborator

schungx commented Jan 14, 2025

Not a normal closure. The only thing you can do is to register an Arc<F> where F is Fn + Clone + 'static. Then Rhai will treat it as a normal type.

But you need to register your own call API...

I can probably add this support into FnPtr but not sure if it'll be useful or only a niche.

EDIT: If added, the functions are likely required to have a specific signature, e.g. Fn(&NativeCallContext, &[Dynamic]) -> Result<Dynamic>, and wrapped in Arc's... if that's OK for you

@makspll
Copy link
Author

makspll commented Jan 14, 2025

Hmm. I'll give you some context so tou can evaluate the need for this better in deciding if you'd like to support this.

Having the ability to store a function with a specific signature and having to override the calling mechanism sounds perfect actually. The following is how this looks like in Lua:

https://github.com/makspll/bevy_mod_scripting/blob/c109fb610e83df36e236fb62977873894b5f6241/crates/languages/bevy_mod_scripting_lua/src/bindings/script_value.rs#L101

And the signature for create_function is just:

Fn(&Lua, A: FromLuaMulti) -> Result<Function>

So it should work, having an FnMut version would be ideal too.

The general need arises in the context of function reflection. The moment you forgo compile time knowledge about functions, you gain immense flexibility at the cost of.. well losing the generics et al. I've been working on moving bevy_mod_scripting over to a dynamic calling model.

@schungx
Copy link
Collaborator

schungx commented Jan 15, 2025

It is probably simple to embed a closure within a FnPtr ... Trick is to make the API ergonomic. Let me experiment with that...

I can see value in what you're suggesting. Those functions don't need to be exposed to scripts.

@schungx schungx linked a pull request Jan 18, 2025 that will close this issue
@schungx
Copy link
Collaborator

schungx commented Jan 18, 2025

@makspll check out the latest drop with FnPtr::from_fn and FnPtr::from_dyn_fn.

@makspll
Copy link
Author

makspll commented Jan 18, 2025

That was quick, awesome!

@schungx
Copy link
Collaborator

schungx commented Jan 20, 2025

@makspll I wonder how you find the new feature. Does it do what you want?

Please share your usage experience if possible. I'm trying to put some usage scenarios into the Book.

@makspll
Copy link
Author

makspll commented Jan 21, 2025

Hey! So I tried this out, and the actual bit of storing and calling the function works perfectly, thanks again!:

Here's me converting my dyn functions to dynamic vals

            ScriptValue::Function(func) => Dynamic::from(FnPtr::from_fn(
                func.name().to_string(),
                move |_ctxt: NativeCallContext, args: &mut [&mut Dynamic]| {
                    let convert_args = args
                        .iter_mut()
                        .map(|arg| ScriptValue::from_dynamic(arg.clone()))
                        .collect::<Result<Vec<_>, _>>()?;

                    let out = func.call(convert_args, RHAI_CALLER_CONTEXT)?;

                    out.into_dynamic()
                },
            )?),

Then I can do:

let type_fn = world["get_type_by_name"];
print(type_fn);
print(type_fn.call("TestComponent"));

output:

Fn(get_type_by_name)
Hello!

However, the bit that's missing now is allowing to override the dispatch of functions on a type, using an indexer works but is fairly counterintuitive,

Ideally I'd be able to either return functions from the indexer, or have a separate function dispatcher of some sort, so that I could do:

world.get_type_by_name("my_type")

Edit: actually i was returning a function without any curried arguments so maybe that was the problem

@schungx
Copy link
Collaborator

schungx commented Jan 21, 2025

In order to use the style world.get_type_by_name("my_type"), world needs to be an object map. It is a built-in functionality.

Otherwise you'd need to do world.get_type_by_name.call("my_type")

Properties default to access via indexers if they are not defined, so the prop shortcut should work just fine. You don't have to always use the indexer notation.

@makspll
Copy link
Author

makspll commented Jan 21, 2025

Hmm I see, would that mean that the object would need to be "pre-populated" with all the functions when it's passed into Rhai?

The world.get_type_by_name.call("type") syntax is not too bad, and should get me going for now though!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants