-
Notifications
You must be signed in to change notification settings - Fork 162
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
AGS 4: implement dynamic cast instruction in the engine #2665
base: ags4
Are you sure you want to change the base?
AGS 4: implement dynamic cast instruction in the engine #2665
Conversation
d90dcee
to
cc7e339
Compare
Okay, so I'll provide an 'as' operator in the compiler. I'm wondering about the technical side of that. My poor puny git-fu isn't up to such a task. What we'd need is a kind of a PR for your PR so that I can send some compiler changes over to you and if you like them, you can integrate them into your PR and then at a later stage integrate your PR into the AGS repository. There's probaby a way for this, but I've never been involved in such a case, and I'm unsure what I have to do to make it happen on my side. |
First, there's a "patch" feature in git that creates a file with code diff from a commit or range of commits: Second, I think on github there's a way to give a permission to push more commits to PR to somebody else, but I've never used that myself. Third, the dumb way is for you to fetch my working branch in your repository (you'd have to add my repository as another "remote" at your local git repo). Then you will be able to push more commits on top and push the whole branch to your personal remote, and I will be able to see and read your changes same way, and cherry pick new commits. |
cc7e339
to
a43ae7b
Compare
This directive declares a "module name", suggested to be used as a sort of a namespace for the RTTI table items. Only the first met "#module" directive is respected, any further uses of this directive are ignored.
Compiler parses an optional appendix to the new script marker, which defines a "module name". This module name is used when compiling RTTI table, all types declared within a section are assigned to that "module", instead of a section. When no module name is defined, compiler falls back to using section name instead (which is usually a name of a script file).
Compiler parses an optional appendix to the new script marker, which defines a "module name". This module name is used when compiling RTTI table, all types declared within a section are assigned to that "module", instead of a section. When no module name is defined, compiler falls back to using section name instead (which is usually a name of a script file).
This is to let find builtin types by their simple names, without "location::" prefix.
a43ae7b
to
fdd5b4e
Compare
For #2018, but only implements the dynamic cast on the engine side; compiler is still lacking cast syntax support.
This implements a dynamic cast instruction in the engine, meaning a cast between two managed pointers performed at runtime, when the engine is capable to know which type exactly given pointers refer to.
I had to do a number of other changes just to make this work, both in compiler and engine.
It will take some time to explain everything, so please be patient reading this... Here's what was done and why.
Matching runtime types in engine with RTTI
The types in RTTI have bare names, and fully qualified names, where fully qualified name has a form of "location::type" ("location" is just a term for a namespace). That is done because in theory there may be multiple types of same name but in different scripts (if they are inside scripts, and not in headers); in which case fully qualified name lets distinguish those. Normally "location" is taken from a script file's name (header or script body), since there's no explicit way in AGS to define a namespace.
Now, if we want to match types known by the engine and plugins with types from RTTI generated by the script, we meet a problem: at compilation time these types may be declared in a script header of any arbitrary name. This means that if we want to find these types in RTTI we cannot rely on the script name at all.
One may assume that engine API is always included, so if you find any type of matching name, then it must be engine's type. But firstly that's not 100% guaranteed (it's potentially possible to compile a script without adding any API header at all). And secondly, that does not solve the problem for plugin types, as we simply do not know which types did plugins declare, because plugin API does not provide any means to tell that to the engine (which may be an oversight in plugin api design, but that's another story).
This brought me to idea, that the script should have means to explicitly declare that certain script contains types from the engine or plugin.
How did I try to solve this here:
I introduced a new preprocessor command called
#module
. This command lets specify the "module", or in other words a "group", where all the types declared within the given script should be assigned to. When preprocessor finds such command#module modulename
, it updates the__NEWSCRIPTSTART_
marker in the script's beginning, appending the module's name to the script's name, separating them with semicolon, e.g.__NEWSCRIPTSTART_ScriptName.ash;modulename
.When the script compiler later parses the script and finds this marker, it saves this appendix as a "module name" along with the "section name" when compiling the script. Section name is used for reporting compiler messages, as usual, and module name is currently used only when generating RTTI table. When writing a list of types in RTTI, compiler would use module's name as a type's "location", if it's available, instead of a section name.
The engine API header has a new directive:
#module ags
, where "ags" is going to be a standard name for anything declared by the engine. This makes all types declared within the builtin header to get into "ags" group.The plugin API headers have following directive prepended:
#module pluginname
, where pluginname is a plugin's file name without extension, e.g.#module agsspritefont
. This name can be easily matched with the plugin name used by the game data and the engine at runtime.When the engine loads a game up, and generates joint RTTI table, it will create lookup aliases for all types from group "ags" and groups related to plugins (using known plugin names). These aliases are simply bare names of types, without any "location::" prefix, and they will be used during dynamic casting, as explained in a following section.
Dynamic cast
Introduced a new script opcode SCMD_DYNAMICCAST = 76, which has 1 argument. This opcode assumes that MAR register contains a managed handle, and arg1 (literal) is a local typeid of a type which you want to cast to. (Local typeid is script-relative, these are mapped to global typeids in joint RTTI table.) It is supposed to keep same handle in MAR if cast is successful, or write 0 if it was a failure, simple like that. (There's no need to write another handle, because handle is always same, regardless of how script "sees" the variable.)
The implementation of SCMD_DYNAMICCAST step-by-step is this:
Changes to dynamic managers
As may be seen from above explanation, the crucial part of the cast for builtin type is being able to get typename from dynamic manager. This also requires that our dynamic managers in the engine return typename exactly matching their counterparts in script. And not only that, but also we must make sure that each builtin object is registered under precise typename, not some "generic" type name.
Many of the existing managers have this right, and their GetType() method return matching strings. But there were 2 kinds of managers that do not:
Therefore I had to make changes to this (keeping in mind to also handle old names in serializer, in case an older save is loaded).
Testing
It's difficult to test this properly without script compiler featuring cast syntax. But only for the draft period I've made 2 demo script functions:
First function tells if type1 can be cast to type2. Note that you must pass a fully qualified name there to make it work with custom types. This function can only test upcast (from child to parent), it cannot be used to test downcast, because there's no object to check the real type.
Second function lets you pass a GUIControl pointer and try cast it to any subclass. For example:
The function will return if the control pointer actually points to the given subclass.
TODO