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

Scripting RFC #1

Merged
merged 2 commits into from
Apr 5, 2019
Merged

Scripting RFC #1

merged 2 commits into from
Apr 5, 2019

Conversation

Moxinilian
Copy link
Member

This RFC introduces our scripting proposal.
Rendered

@kabergstrom

@Moxinilian Moxinilian changed the title Create 0001-scripting.md Scripting RFC Oct 28, 2018
@Xaeroxe
Copy link
Member

Xaeroxe commented Oct 31, 2018

I really like all of this. My only suggestion is a name change, rather than augmented FFI we should call it Automatically Idiomatic Foreign Function Interface. AI-FFI.

@fhaynes
Copy link
Member

fhaynes commented Oct 31, 2018

The first one is to ban scripts from handling their own threading.

What about languages that use green threads?

@Moxinilian
Copy link
Member Author

Feel free to use GitHub's review features so we can have threads.
@Xaeroxe Sure, why not, I called it augmented FFI just for the context of the RFC, I wasn't planning on keeping the name.
@fhaynes I think this could fall under the "limited managed threading" kind of deal. But maybe it's been too long since I've read about green threads. I need to look at it again.
But like for example it would be perfectly fine to have Rust have threads, because Rust is smart. The constraints here are language-specific.

@fhaynes
Copy link
Member

fhaynes commented Oct 31, 2018

Feel free to use GitHub's review features so we can have threads.
@Xaeroxe Sure, why not, I called it augmented FFI just for the context of the RFC, I wasn't planning on keeping the name.
@fhaynes I think this could fall under the "limited managed threading" kind of deal. But maybe it's been too long since I've read about green threads. I need to look at it again.
But like for example it would be perfectly fine to have Rust have threads, because Rust is smart. The constraints here are language-specific.

I mean more like threads that are managed by a language runtime, goroutines or Erlang processes for example, but the OS views as just one OS thread.

@Moxinilian
Copy link
Member Author

It would depend on specific cases then. I haven't thought about it yet. If you want to make an addition, please write a paragraph and PR it to my fork.

@fhaynes
Copy link
Member

fhaynes commented Oct 31, 2018

In order to expose that API in an idiomatic way to the language they drive, the drivers are also provided with additional high level metadata, along with the FFI. For example, if a type is an iterator, the language driver will be noticed of this additional information so that it can, for example, do the necessary language-specific work for this iterator to be iterated over in a for loop (see the Lua example for more details).

Something about this bugs me, but I'm not sure what. Will have to think about it more. At a minimum, I think this will need a strict schema. I just have a suspicion that the driver code responsible for the the translation will end up messy. But we won't know until we try it.

0001-scripting.md Outdated Show resolved Hide resolved
0001-scripting.md Show resolved Hide resolved
@fhaynes
Copy link
Member

fhaynes commented Oct 31, 2018

It looks pretty solid to me. I'd vote move forward with a minimal PoC using LuaJIT to get an idea of the difficulties associated with this level of genercism.

Copy link
Member

@torkleyy torkleyy left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is pretty similar to what was said before. I'm going to go through the details later.


Anyway, keep in mind that what approach is chosen can be different for each language, as different languages give us more or less flexibility (Rust as a scripting language for example already enforces all of this out of the box).

## Hotswapping
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that script system(s) are run in a separate World(?). I don't remember where I read it but since there is no way to add or remove systems after constructing a World it was recommended to keep dynamic systems in a separate world to avoid needing to reconstruct everything everytime something is reloaded.

https://github.com/Marwes/shred-example

https://github.com/Marwes/shred-example/blob/9f6d6a62f0b4cf7f55c3e5e9ca851c76c8ec9dea/src/gluon_system.rs#L798-L800

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The architecture Specs will have in the future will differ from the current quite a bit. It's already in design ;)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regarding your comment, I think you're mixing up Dispatcher and World. Both are very likely going to change (the latter one also renamed to SystemGraph which will not need any of those workarounds).

We will quote here what I said in slide-rs/specs#462 regarding this issue:

* Creating a new component type at runtime could be handled by having a special non-generic storage type that would store `Vec<u8>`. Those `Vec<u8>` would contain the raw data of the new components. Then, those components would be exposed to the scripting runtime through newly generated C headers corresponding to the component schema the user provided. That however will mean that TypeId can no longer be the only thing determining ResourceId, as those storages would share the same type but a different ID. But it does not seem unreasonable to have ResourceId become a `(TypeId, InstanceId)`, at least from my uneducated point of view. Also, at the end of the execution, the dynamic component should be collapsed into a statically generated component to no longer need to store the data in a `Vec<u8>`. The typical wokflow would be: create your components on the fly, do your testing with it, and if it's too slow then reboot the game. It seems reasonable to me, and besides I don't really expect it to be that slow, especially if you're only using it for logic creation.
* Modifying existing component types on the fly would only work on component types that were created from the scripting environment, but I think it's a reasonable constraint. Modifying a component in dynamic state would simply be a matter of iterating over the `Vec<u8>` storage and moving the data inside around. Modifying a component in statically-collapsed state however is a bit trickier, but not that hard either. You need to replace all static instances of that component into a dynamic instance of the new component.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Modifying existing component types on the fly would only work on component types that were created from the scripting environment, but I think it's a reasonable constraint.

Does this mean that scripts would not be able to modify core components like Transform? That seems like it would reduce the power of scripts quite a bit. Is there any reason that component methods couldn't be exposed? Or is there another way that scripts might access static components that I'm missing?

Copy link
Member Author

@Moxinilian Moxinilian Feb 19, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What I meant is that scripts can't change the type signature of components defined outside of the scripting environment, as in they can't modify them. In other words, if your project creates a Rust struct as a component, you won't be able to hot-reload its structure definition.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, you meant the component types, not component instances. So scripted systems will be able to read arbitrary storages from the world and modify the components in those storages, correct?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not the systems. Scripted systems can do exactly as much as Rust systems.
But from the scripting ecosystem, you can't edit static Rust types.

@jmqualls
Copy link

My 2c as someone who is not (yet) a contributor but a prospective user:

A bird in the hand is worth 2 in the bush. Don't design an API for all languages up front. Just the fact that there exists an API separating the engine and the language is sufficient abstraction for a first pass.

Make the first language a mature one. Don't adopt someone else's technical debt. LuaJIT seems like a fine choice.

Once people can actually write and run some kind of script at all, then start thinking about what kinds of accommodations would have to be made to support MUMPS or Befunge or whatever. Don't be afraid to say no if the language concepts just don't map well to the engine data structures.

@Moxinilian
Copy link
Member Author

Could you give more details on the reasoning behind that conclusion?

@jmqualls
Copy link

jmqualls commented Mar 1, 2019

Could you give more details on the reasoning behind that conclusion?

http://wiki.c2.com/?PrematureAbstraction
http://wiki.c2.com/?TracerBullets

@Moxinilian
Copy link
Member Author

I published a post on the Amethyst forums containing more details on the LuaJIT implementation.

@Moxinilian
Copy link
Member Author

@jmqualls I wouldn't call premature a solution to a problem all major game engines have failed at solving because of their legacy code and userbase. By only thinking about a single language, we get ourselves trapped in that language and might eventually be stuck with it. Unity is so stuck with C# that they ended up creating a custom compiler for it when they realized the language requirements had changed.

Besides, so far, this general purpose solution has not been in the way at all of the LuaJIT integration. If you want to consider this + LuaJIT as focusing on a single language first, then it's all good! It isn't very complicated either, we are just describing the essence of an ECS and the requirements for it to fit to a language.

@DianaNites
Copy link

One possibility is to have something that other languages can compile to easily.

Even Lua can fit this purpose, with several languages that can compile to it, including C#. Typescript to Lua compilers are particularly interesting to me, basically free types and other nice things, and much better IDE support than plain Lua.

@Moxinilian
Copy link
Member Author

@DianaNites My issue with this is that it would introduce one more potentially costly abstraction layer. For example, say we want to integrate Go with this method, we cannot take advantage of its pretty performant static builds. Also, this does limit what languages we want to integrate, as we wouldn’t have the opportunity to maintain different compilers to Lua.

But this approach is not incompatible with the one suggested in this RFC, so if it makes sense in some casss it can definitely be used.

@fhaynes
Copy link
Member

fhaynes commented Apr 5, 2019

Merging this as @khionu is satisfied.

@fhaynes fhaynes merged commit 0fad6d0 into amethyst:master Apr 5, 2019
@zicklag
Copy link

zicklag commented Jun 7, 2019

When does the builder phase happen in this design? The build is taken care of automatically by Amethyst or other Amethyst tooling without you having to re-compile Amethyst itself, right?

I want to understand whether or not I can build Amethyst itself one time, distribute that to users, and then allow them to write games with just the scripting languages and the Amethyst binary ( even if that includes more tooling like an Amethyst CLI ).

@Moxinilian
Copy link
Member Author

You are correct. The build is, at least during development, a dynamic process. If the language is a dynamic one (like Lua for example), then it would work very simarly to how it is commonly done. If the language is a more static one (like Rust for example), it’s a module of the engine that builds your code and dynamically links it without even needing to shut down the engine.

@erlend-sh
Copy link
Member

erlend-sh commented Jul 26, 2019

For those following along, the implementation for this RFC is being discussed here:

The implementation repo lives here: https://github.com/katharostech/amethyst-scripting-lab

@erlend-sh
Copy link
Member

A proof of concept for this RFC can be found here:

https://github.com/redcodestudios/rust-scripting-example

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

Successfully merging this pull request may close these issues.