Script Type Schema v1

(Khionu Sybiern) #1

I’m creating this thread to consolidate the discussions on the schema used to communicate types and typing to and from the Scripts and Scripting Engine.

Design points that should to be answered:

  • Are there any existing solutions that can be used with/optimized to ideal performance?
    • JIT the types from schema as part of the engine loading?
    • Can we pull them from the scripts themselves?
    • Leave it to the Language Driver to handle the schema for communication between the Engine?
  • How do we want to handle potential overlap in types?
    • How do we do types shared between scripts, so scripts can be dependent on each other?
1 Like

(Théo Degioanni) #2

The general idea I had for this was to have a static schema asset that described types. Then, scripts refer to them externally. I believe this would be the best solution, as it is handled by the powerful asset pipeline, which means it can be dynamic during development and statically generated for deployment performance.


(Khionu Sybiern) #3

Could you elaborate how this would look? I have a very vague idea of the asset pipeline, but I’m not entirely sure how it would look here, as more solid picture.


(Jaynus) #4

In all of the discussions/documents I’ve seen so far, the concept of a ‘script’ is discussed as a unique ‘thing’, with no regard for the concept of a runtime. When I see ‘dependent on eachother’, I become wary because this is conceptually introducing interdependence between working units (scripts) which co-exist already in a given runtime (Driver). The concept of types and schema should really be interdependent, as one defines the other - so I don’t really see the need to handle overlap/dependency, as the ‘types’ should be considered a shared state between all.

Whew, ok. Now with that out of the way. I am very wary of pre-defined component definitions schemas (the concept of having to pre-define all types in a file and edit them there) unless the process is completed automated. That is to say, if my rust AND all other language definitions are generated from that schema, its fine. However - this adds a huge layer of complexity and limitations on the rust data itself, and on what is described for the engine.

Personally, I am partial to Flatbuffers reflection for the schema, and possibly flatbuffers itself for some/all type interference and marshaling. Rather than discussing the merits of it, I am going to discuss the benefits of it abstractly, as I think thats part of the discussion. So without further rambling, here are the reasons its my fairly uneducated first go to.

  • It has a native rust implementation, requiring no bindings
  • It ALSO has native implementations in: lua, javascript, c#, c++, go, python, TypeScript, php, c, java…
  • It has a self describing reflection: (Ex: which can be extended and modified for our purposes if so required (although it already has an attributes feature, which can be used for the metadata such as iterators)
  • It is zero-copy and unserialized, so data is read as-is with zero-cost abstractions
  • It is expressive in both binary and text formats, allowing for FFI usage and user-editing
  • It allows codegen to many languages, keeping type safety possible in rust and others where applicable
    It supports flex building (building arbitrary messages) as well as strongly typed (defined via a fbs definition), for further flex(ability).

Now, how did I mentally envision this possibly tying in with FFI? A few ways:

  • Schemas could exist in text and/or binary format for various build types (debug, develop and release packaging). These could also be statically baked for release builds
  • Schema transfer to different drivers could be done in a binary fashion with simple FFI functions, returning the Reflection struct via FFI being simple
  • This abstracts away the concept of a ‘schema’, and allows us to treat it as just another data type
  • In regards to specs, this allows the storage of arbitrary component types (flatbuffers buffers) in a Vec variant, while providing us with strong typing auto-generation features. Additionally, it allows rust to still access this components, so they arn’t opaque (Flexbuffer access /w the reflection can be used).

Off the top of my head, I’m not aware of any other data schema language which provides both the description of data and the read/write of data in such an opaque format. If there is one, without hte overhead, I’d love to see how it compares.

1 Like

(Khionu Sybiern) #5

So, how would this look for scripts declaring their components, which would then need to be added to a storage?


(Théo Degioanni) #6

This post should answer your questions on what I have already designed. If not, I shall explain it more.


(Jaynus) #7

@khionu - I’ll lay out how I see the flow details working, as implementation details to the RFC. As a thought experiment, I’m approaching this implementation from the perspective of reflection, and not scripting. The problem space is much more reflection oriented than it is a ‘scripting engine’ problem, as the core issue is communicating functionality and data structures between disparate systems.

Amethyst Proper -

  • All engine functionality which is to be exposed is done so in an automated fashion, using proc macros to generate automatic FFI interfaces. This is not a catch all solution, of course, but will cover a large set of functionality. Anything that needs custom interfaces can have them defined.

  • This same proc macro will additionally generate flatbuffers reflection definitions of the FFI interfaces. This automagically gives us an FFI interface with a matching reflected definition set.

  • These definitions, during build, can then be used to generate both runtime and compile-time data structures and definitions.

  • This proc macro could be integrated with serde, to leverage it for portions of serializing if so desired. I don’t know if this is a good direction though.

A Driver Consuming the Engine

  • A driver can now use these reflection definitions to generate its appropriate bindings for its given language. These definitions, additionally can be transported via an FFI interface or via a binary or human readable format.
  • Inversely, a script runtime can pass reflection data to the engine to expose its functionality or supported features as well.

A Script generating components

  • When conducted at runtime, a flatbuffer definition file is sent to the engine, which can then consume the reflected definition and use the data structure. This can then be code generated on next reload, creating actual rust bindings. Before this, though, flatbuffers provides the ability to use a reflection definition to access a data buffer.

  • These dynamic components can be stored in the generic Vec specs storage for this first use until it becomes a code generated rust type.

Final Thoughts

  • This reflection schema opens the door to a lot of other interesting future opportunities; such as engine-wide runtime and compile time reflection, dynamic plugins and dispatch, and other interesting opportunities beyond scripting.

  • Instead of thinking of a script as a singular unit, I am thinking of interconnected runtimes - how do we expose the ability to use the power of the engine in any language.

  • As I stated at the beginning - as a thought experiment, I am approaching this as if it was a reflection problem. I’m attempting to abstract away from the concept of a ‘script’ and a ‘driver’ and an ‘engine’, and instead approaching this as exposing functionality between both; how can we get scripts and the engine to seamlessly mesh in a abstract and intuitive way?

Reading the above mentioned write up on an ECS schema, this is where I think my opinion drastically differs - as I am attempting to mesh runtimes, not strictly the ECS. I feel defining strictly an entity/system/component definition type to a script is only part of the problem and only part of the solution

HOWEVER, I’m new around here - so I’m sure there were previously discussed constraints around this I may not be aware of. I’m attempting to formulate and express what an ideal reflection and runtime integration system would look like. If amethyst plans to strictly approach scriptin as a unitary element, thats cool too. I could see extending some of this in other areas for use as well.

1 Like

(Théo Degioanni) #8

What would be the advantages to defining types with flatbuffers over than something like C-bindgen? I can see disadvantages: for example, cbindgen allows us to translate any possible type thanks to C headers automatically, even in the standard library or third parties. While flatbuffers are well supported, C headers are even more universal. The initial idea was that the engine would parse a schema and generate an actual Rust struct that is exposed with “augmented cbindgen” (in order to give information on say Add or Eq traits). This already covers any Rust type without any effort.

Speaking of parsing, why runtime? This just adds a boot overhead and makes it harder to statically optimize scripts.

In the original idea, scripts like type schemas are assets in the asset pipeline meaning of it (see the related RFC). Types are declared then used by one or multiple scripts. Scripts do not depend on each other so there is no need to parse what is inside a script, which would be very difficult.

Also, I don’t understand how your solution generalizes the concept here, I only understand that you are suggesting using flatbuffers instead of native interfaces. Could you detail that more?

1 Like

(Dave Kushner) #9

I’m joining in here half-way so please excuse me if I’ve missed some documentation surrounding this that lies elsewhere. Has anyone documented the end-to-end life-cycle of a script from source to runtime execution? There are numerous mentions of the new asset pipeline but I’m having trouble visualizing the user experience that is the end goal here.


(Jaynus) #10

Cbindgen only solves a small part of the problem, mainly generating an ffi header from already written interfaces; so I see requiring writing an entire amethyst ffi implementation no? Next, that interface requires a custom schema written and maintained in parellel. Next each languages parser of that schema has to be mantained. Finally, “script” is now limited to a predefined entry point set as per designed and is not extensible - limiting as I said previously to a unitary “script” design, and not a homogeneous runtime

You may have misunderstood the runtime portion, as that’s just one facet - it compiles to native structures in each language as well. It was just a bonus for ease of hot usage of the data. Additionally, I am not advocating using it for communication or serialization, but as a pre-existing solution to everything I just mentioned above.

I only offer one posssible solution to the problems I mention here, as these were my reservations with the RFC as written as it is too abstract to speak on these problems; none of them are elaborated nor addressed anywhere I can find.


(Khionu Sybiern) #11

So, apparently Flatbuffers don’t support Generics? That rules it out quite finally, unless you have a potential solution?


(Joël Lupien) #12

Nor does ffi. There are still a lot of languages that don’t support generics, and most of them don’t support trait bounds as complex as rust. Generics are the main reason I’m not using flatbuffers or protobuf everywhere today.


(Khionu Sybiern) #13

The idea originally thought of was that we would support it in a custom schema, which would use ffi as a basis for communication, generating wrappers on both ends. It would end up being 0 overhead on the Rust side, and potentially 0 overhead on the script side.

That said, at this point, nothing is concrete.


(Théo Degioanni) #14

We are discussing type representation here, and potentially storage. FFI is just how this is exposed.
We want to translate schema types to Rust types that are then translated into C headers. This supports generics, at least enough.


(Jaynus) #15

Thats fine, but a solution which solves those problems would still be ideal - as the majority of the space which I would attempt to solve with it still exists.


(Kae) #16

The most commonly supported ABI is C. I can’t think of a language that does not support C FFI, so it should be considered the lowest common denominator.

What is the scope for this “Script Type Schema”? Does it include editor type reflection for the purpose of editing components? Does it include serialisation of components/assets? Is it for calling functions and communicating data on the stack or does it extend to serialising messages etc? Which Rust types and std types would be supported? Would serialisation support identities for objects in a message, unlike Unity?

1 Like

(Khionu Sybiern) #17

This thread was started to work on a specific scope of implementation specifics. However, due to some contentions and meta review, we have concluded that we need to take the design process back a step, and re-approach the Scripting Engine in a more problem-oriented fashion.

For now, I will be closing this thread. Thank you everyone for your input to this point.

1 Like

(Khionu Sybiern) closed #18