JamesBoer / Jinx

Embeddable scripting language for real-time applications
https://jamesboer.github.io/Jinx/
MIT License
305 stars 11 forks source link

Calling a function from C++ #3

Closed ghost closed 5 years ago

ghost commented 5 years ago

Hi, is it possible to call a Jinx function from C++, in case of an event based API? And, ideally, check if the function was defined in a script in the first place?

JamesBoer commented 5 years ago

Not at the moment. My own use case so far has been to call native functions from Jinx scripts, not the other way around. But it seems like this is pretty basic functionality most people would expect from a scripting language. I'll go ahead and look into adding this.

I've also been considering implementing an event-handling system that allows events to be broadcast to all active scripts with designated "handler" functions that match the signature, but I'm still working out a few design details for that.

ghost commented 5 years ago

Thank you very much.

I've also been considering implementing an event-handling system that allows events to be broadcast to all active scripts with designated "handler" functions that match the signature, but I'm still working out a few design details for that.

Sounds cool. Will it be possible to have functions of the same signature subscribed to different and multiple events? Like:

import core

event(explosion, minor incident) function nelson speaks
    write line "Ha-Ha!"
end

event(explosion) function normal person reacts to explosion
    write line "Oh no, a giant explosion, help me, superdude!"
end

event(laser) function superdude
    write line "Nothing to fear, with my powers I can stop anything but las-aaarg"
end
JamesBoer commented 5 years ago

The current design looks more like current function declarations, except with handler replacing function. Return values are not allowed, since it's a one-to-many broadcast. When the handler function is called (the syntax looks like any other function call), any currently executing script will have its own handler called.

ghost commented 5 years ago

any currently executing script will have its own handler called

So you are planning to have one handler per Script object?

JamesBoer commented 5 years ago

Scripts can have any number of different handlers. I was referring to how a signal sent out would execute on any script that happened to have a matching handler (and there would only be one of these).

Again, this is all a theoretical design with no planned timeline. In contrast, I should have normal C++ to script function calls in soon. The basic functionality is done, and I just need to write tests and documentation. I'd guess I'll be finished by the end of this weekend.

ghost commented 5 years ago

In contrast, I should have normal C++ to script function calls in soon. The basic functionality is done, and I just need to write tests and documentation. I'd guess I'll be finished by the end of this weekend.

That sure is fast. Thank you for implementing it.

JamesBoer commented 5 years ago

Not a problem. So far the solution seems to be solid, so I'll move it to the main branch soon. Let me know if you notice any issues for have any suggestions for improvements.

ghost commented 5 years ago

Let me know if you notice any issues

Will do. That being said, due to the erratic pace in developing my chaotic roguelike it's hard to tell when I get to actually testing this.

or have any suggestions for improvements.

It's not my use case, but for the sake of completeness, I noticed that Jinx apparently supports incremental execution, like shown in this tutorial snippet:

do
{
    if (!script->Execute())
        return; // Execution runtime error!
}
while (!script->IsFinished());

Have you considered this for function calls as well? For example, the API could work something like this:

// [...]
// Prepares the stack, instruction pointer and such...
script->StartFunctionCall(id, { 5, 2 });
do
{
    if (!script->Execute())
        return; // Execution runtime error!
}
while (!script->IsFinished());
// Optionally get the resulting value.
auto val= script->GetResult();

Admittedly, that's not exactly the elegant pinnacle of pure functional programming.

JamesBoer commented 5 years ago

That's an excellent point. However, a person would have had to introduce this behavior with a wait keyword inside the function. That usually indicates waiting for some condition to finish until continuing execution inside the script, so I'd argue that executing once is still probably the correct behavior. At that point, I think I'd be okay with discarding the return parameter.

Generally speaking, you'd never really write an Execute() loop like that. All the examples that do this are strictly for testing purposes. The typical use case in actual code is to call Execute() once per game loop, enabling scripts to operate asynchronously, as co-routines.

That being said, I'll need to write some tests and verify this works as I think it should. And I should certainly mention this in the documentation. Thanks for the feedback.

ghost commented 5 years ago

enabling scripts to operate asynchronously, as co-routines.

Ah, I erroneously assumed it was supposed to be called each update in order to execute a fixed number of instructions. And it made me wonder how you got that throughput in the benchmarks. Thanks for clearing up this misunderstanding.

edit: and that's why I dislike markdown

ghost commented 5 years ago

@JamesBoer BTW, if I understood it correctly, the wait keyword is basically a latent function, as described in here and used in UnrealScript and Papyrus. What is your take on custom latent functions, like wait until players health is(50)?

This is probably something embedders can build themselves using wait in a custom library function and handling the remaining parts in the C++ loop. I'm also not sure if the parser/compiler would gain any advantage if it knew that a function is latent; wait can be called anywhere, after all. Any thoughts?

JamesBoer commented 5 years ago

You can certainly write a function with a wait command inside it. Alternatively, Jinx supports conditional waits to make things like this even easier, using either:

wait while <expression>

wait until <expression>

So, given a function players health that returns a value, you can write

wait until players health <= 50

And the script will dutifully pause execution until the players health drops at or below a value of 50. This is why you should consider ensuring every Jinx script is attached to long-running game objects. If you buy into this paradigm, making sure your scripts call the execute function once per frame indefinitely during the game loop until they're finished, writing these sort of latent functions becomes very easy. For example, here's a shared library function in my own game engine:

public function wait for {number x} second/seconds
    set t to x + current time
    wait while current time < t
end

current time is a native callback that retrieves the current time in seconds.

ghost commented 5 years ago

@JamesBoer That's pretty cool.

I am testing single function calls right now and found a problem with the FindFunction method API:

It takes the signature as initializer list. However - and I don't think that's too exotic - I load the function name from a config file, for each entity type that needs event handling, which isn't possible to pass as initializer list. Converting a vector to an initializer list doesn't seem possible, either.

ghost commented 5 years ago

To elaborate, the idea behind this is to dynamically load callback functions from files like

{
  "items": [
    {
      "name": "small health injection",
      "on_consume": "consume small health injection",
      "on_pick_up": "pick up small health injection"
    },
    ...
  ]
}

to enable data driven development as far as possible.

JamesBoer commented 5 years ago

Honestly, I'd already been thinking that the initializer list seems like more trouble than its worth. Would it make things simpler for you if you could just pass the function parts in a single string? I think it would look a lot cleaner for hard-coded strings, and would also be easier in cases where you want to pass things via more data-driven methods.

ghost commented 5 years ago

Would it make things simpler for you if you could just pass the function parts in a single string?

Definitely. It seems there is no go-to way to split strings in C++.

Will you keep initializer lists for registering functions or is there no guaranteed API stability until 1.0?

edit: I'm asking, because iirc somewhere on your homepage you mentioned that 1.0 is likely to be released at the end of this year anyway...

JamesBoer commented 5 years ago

I think I'd prefer to remove the initializer lists before 1.0. I've actually been bitten with my own mistake when I accidentally forgot a comma, and it was a real bear to spot that, because C++ just silently concatenates strings. I hadn't even considered that it would make passing function declarations from data problematic. Best to make those changes now, I think.

ghost commented 5 years ago

I accidentally forgot a comma, and it was a real bear to spot that, because C++ just silently concatenates strings.

Wow, C++ sure finds interesting ways to turn language features into obstacles.

ghost commented 5 years ago

Thanks, it now works as expected.

JamesBoer commented 5 years ago

Cool, closing this issue then.