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

AGS 4: implement dynamic cast instruction in the engine #2665

Draft
wants to merge 9 commits into
base: ags4
Choose a base branch
from

Conversation

ivan-mogilko
Copy link
Contributor

@ivan-mogilko ivan-mogilko commented Jan 22, 2025

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:

  • take object handle from MAR register;
  • if handle is 0, meaning null pointer, then do nothing (null always casts to null);
  • use handle to get the dynamic object and its manager from the managed pool; if none is found, then fail, writing 0 to MAR.
  • ask manager about the object's typeid. Only "user structs" currently can return that, others return failure. (I suppose this may be improved at some point, it's strictly implementation problem.)
  • on failure, ask manager about the type name, and use this name to find global typeid in a lookup table. That's what these aliases were meant for.
  • if nothing was found (that's weird), fail, writing 0 into MAR.
  • when we got real object's typeid, this way or another, we check whether the real typeid matches the type we cast to, OR if any of that type's parent typeid matches the type we cast to. If any of that is correct, then the operation is a success.
  • otherwise it's a failure, and we write 0 into MAR.

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:

  1. Some managers had historically slightly different names (like "GUIObject" vs "GUIControl"), and some were renamed when their save format changed at some point in the past ("Camera" and "Viewport"), which was a dumb move on my part.
  2. GUI control subclasses all uses same GUIObject manager.

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).

  1. All managers made to return typenames precisely matching the struct names in script API.
  2. Created subclass managers for each subclass of GUI and GUIControl, and register gui objects with proper manager.

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:

import bool TestDynamicCast(const string type1, const string type2);
import bool TestDynamicCastGUIControl(GUIControl* control, const string new_type);

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:

TestDynamicCastGUIControl(btnMyButton, "Button");
GUIControl *control = lblMyLabel;
TestDynamicCastGUIControl(control , "Label");

The function will return if the control pointer actually points to the given subclass.

TODO

  • - Compiler syntax for casting (separate task?).
  • - Fix dynamic manager for subclasses (gui controls) when restoring an older save. It's possible to do, although may be annoying to code.

@fernewelten
Copy link
Contributor

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.

@ivan-mogilko
Copy link
Contributor Author

ivan-mogilko commented Jan 23, 2025

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.

First, there's a "patch" feature in git that creates a file with code diff from a commit or range of commits:
https://git-scm.com/docs/git-format-patch

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.

@ivan-mogilko ivan-mogilko force-pushed the ags4--scriptdynamiccast branch from cc7e339 to a43ae7b Compare January 29, 2025 12:59
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.
@ivan-mogilko ivan-mogilko force-pushed the ags4--scriptdynamiccast branch from a43ae7b to fdd5b4e Compare January 31, 2025 20:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants