Intergen - Outerra/anteworld GitHub Wiki
This document describes the intergen tool for generating C++/Javascript/Lua APIs from C++ classes. The naming convention used here:
host class - any application class, can have one or more interfaces declared
client interface class - generated thin wrapper class for a particular interface declaration
An example of how the host class could be decorated for intergen to automatically create C++, JavaScript and Lua client interfaces to the same class:
//host class
class engine
{
public:
//declare an interface, declared name is used as the interface class name
// the name can optionally contain a namespace under which it should be created
//the second parameter is a relative path to the generated interface header, with optional interface file name
ifc_class(ot::engine_interface, ”ifc/”)
//non-static interface methods
ifc_fn bool create_entity( const char* name, ifc_out uint* eid );
ifc_fn void delete_entity( uint eid );
//in case you want the interface method to be named differently than the method
// name in the host class, use ifc_fnx(name) to declare it
ifc_fnx(something) int do_something();
//an interface creator static method (see below)
ifc_fn static iref<engine> get( const char* param );
//other normal class content, optionally other interfaces
// ...
};
Running the intergen tool on the header file containing this class definition generates an interface class containing the decorated methods from the host class.
namespace ot {
//interface class
class engine_interface
{
public:
//a static method to retrieve the interface
static iref<engine_interface> get( const char* param );
bool create_entity( const char* name, uint* eid );
void delete_entity( uint eid );
int something();
};
} //namespace ot
The generated client interface class can be used like this:
//usage: create interface object by calling its creator method that internally calls
// host interface creator method
iref<engine_interface> ent = engine_interface::get(“blabla”);
//call a method of the interface
uint eid;
ent->create_entity(“hoohoo”, &eid);
Note that one host class can have multiple interfaces defined by ifc_class
or ifc_class_var
macros. All decorated methods that follow the interface class macro belong to the interface, up until the end of file or until the next interface class declaration.
Interface declaration can occur anywhere within a C++ class, and it can be declared multiple times. Subsequent ifc_fn*
or ifc_event*
markers define which methods belong to the interface.
Keyword | Parameters | Description |
---|---|---|
ifc_class(ifc_name, dst_path, ...) IFC_CLASS(ifc_name, dst_path, ...) |
ifc_name: Desired name of the interface class, optionally with (virtual) base interface [ns::]name [: [ns::]baseifc] [ final] dst_path: Relative path (and optional file name) where to generate the interface header file "...": Optional relative path to the file containing the base interface |
Interface class decoration keyword. |
ifc_class_var(ifc_name, dst_path, var, ...) coid::clean_ptr<intergen_interface> var IFC_CLASS_VAR(ifc_name, dst_path, var, ...) coid::clean_ptr<intergen_interface> var |
ifc_name: Desired name of the interface class, optionally with (virtual) base interface [ns::]name [: [ns::]baseifc] [ final] dst_path: Relative path (and optional file name) of the interface header file var: Name for the variable representing the connected client "...": Additional parameters if any |
Interface class decoration keyword for bi-directional interfaces with event methods that can be overridden by the client. "var" is of type clean_ptr, an intentional weak link, since interface already holds ref to the host |
ifc_class_virtual(ifc_name, dst_path) ifc_class_virtual_var(ifc_name, dst_path, var, ...) coid::clean_ptr<intergen_interface> var IFC_CLASS_VIRTUAL(ifc_name, dst_path) IFC_CLASS_VIRTUAL_VAR(ifc_name, dst_path, var, ...) coid::clean_ptr<intergen_interface> var |
ifc_name: Desired name of the interface class, optionally with namespace dst_path: Relative path (and optional file name) of the interface header file |
Virtual base interface class decoration keyword for declaration of abstract base interfaces. |
ifc_struct(ifc_name, dst_path) IFC_STRUCT(ifc_name, dst_path) |
ifc_name: Desired name of the interface class, optionally with namespace and base interface [ns:]name [: baseifc] dst_path: Relative path (and optional file name) of the interface header file |
Data interface (not refcounted). |
Examples
ifc_class(ns::client : ns::base, "../ifc")
ifc_class_var(ns::client : ns::base, "../ifc", _ifc)
ifc_class_virtual(ns:base, "../ifc")
ifc_class(ns::client : ns::base, "../ifc")
Valid after an ifc_class*
declaration.
Keyword | Description |
---|---|
ifc_fn | Interface function decoration keyword: such decorated method will be added into the interface |
ifc_fnx(extname) |
extname: An alternative name for the method used in the interface Interface function decoration keyword |
ifc_fnx(~) | Marks a method that is invoked when the interface is being detached (client destroying) |
ifc_fnx(!) ifc_fnx(!name) |
Marks an interface method that's not available to script clients |
ifc_fnx(-) ifc_fnx(-name) |
Marks an interface method that is not captured by an interceptor (for net replication etc) if default was on |
ifc_fnx(+) ifc_fnx(+name) |
Marks an interface method that is captured by an interceptor (for net replication etc) if default was off |
ifc_fnx(@connect) | Marks a method that gets called upon successfull interface connection |
ifc_fnx(@unload) | Marks a static method called when a registered client of given interface is unloading |
ifc_fnx(class=...) | Marks a method that returns ifc_class type interface |
ifc_fnx(struct=...) | Marks a method that returns ifc_struct type interface |
Examples
ifc_fn void fn(int a, ifc_out int& b);
ifc_fnx(get) void get_internal();
Interface events are methods not implemented in the host class but expected to be implemented in the clients that connect to the interface. Normally, calling such methods would throw an exception if no client is connected, but if it doesn't matter, it's possible to tell intergen to use a specific method body in case no client is connected (yet).
Keyword | Description |
---|---|
ifc_event ifc_eventx(extname) |
Interface event callback decoration Events are defined in the generated dispatcher code, so the method after this keyword should be just a declaration |
ifc_eventx(@connect) | Marks an event that gets called upon successful interface connection. |
ifc_eventx(=0) ifc_eventx(extname=0) |
Tells that client virtual method will be pure and has to be implemented |
Example
ifc_event void on_init(int a, ifc_out int& b); //definition generated by intergen
For interface methods used from script clients, it's necessary to provide additional info about the direction of method arguments.
Keyword |
---|
ifc_in |
ifc_out |
ifc_inout |
Keyword | Description |
---|---|
ifc_default_body(x) ifc_default_empty |
Statement to fill in as the default implementation of an event, in case the client doesn't implement the event handling. |
Optionally, should be specified after an ifc_event method declaration:
ifc_event bool event() const ifc_default_body(return false;);
Example
ifc_event int on_init() ifc_default_body("return -1;");
Keyword | Description |
---|---|
ifc_volatile | Parameter hint that the storage for the type (usually array) may be external and valid only for the duration of the (event) call or limited |
Use the following style comments to include the enclosed code in generated interface client header file
ifc{
... code ...
}ifc
To push the enclosed code block into the client header, use
ifc{
... code ...
}ifc* /
To push the enclosed code block into the generated dispatch files, use
ifc-dispatch{
... code ...
}ifc-dispatch
//or
ifc-dispatch{
... code ...
}ifc-dispatch
When host class contains multiple interfaces, you can specify which interface header files to include:
ifc{ ns::ifca ns::ifcb
...code...
}ifc
Generated code is normally pushed before the client class definition. In case it needs to be added after, add a + sign
ifc{+
//or
ifc{ ns::ifc1+
Normal methods callable from the interface can be simply declared by prefixing the method with ifc_fn
or ifc_fnx
keyword. Additionally, any out and in-out parameters have to be decorated by ifc_out
or ifc_inout
prefix keywords, respectively.
//host class
class engine
{
public:
ifc_class(ot::engine_interface, ”ifc/”)
//an interface creator static method
ifc_fn static iref<engine> get( const char* param );
//non-static interface methods, out/inout parameters prefixed
ifc_fn bool create_entity( const char* name, ifc_out uint* eid );
ifc_fn void delete_entity( uint eid );
//enum type parameters must have explicit enum keyword
ifc_fn bool something( enum ESomething p );
};
Interface destructor can be propagated as a special method of the host class, decorated by ifc_fnx(~)
. This method is not exposed in the interface directly, it gets called only when the interface is released.
//host class
class engine
{
public:
ifc_class(ot::engine_interface, ”ifc/”)
//a method called when interface client is released
ifc_fnx(~) void destroy();
When a client wants to obtain interface to a host object, it needs to call a special static method of the interface named creator. Creators are static host class methods that allow clients to connect to a host object. There can be several creators defined on the host class, and what object instance they return depends solely on their implementation. One can have creator that returns a singleton object, and so all clients would retrieve interfaces to the same object. Or a creator can create a new instance of the host class, or fetch an existing one based on optional parameters declared for the creator method.
Creator methods are declared as static ifc_fn
decorated methods with iref<host>
return value. No other static interface methods are allowed.
class engine
{
public:
ifc_class(ot::engine_interface, ”ifc/”)
//a static methods returning a reference to the host class are considered
// to be the accessors used to obtain the host object when interface is requested.
//a client would call these methods like engine_interface::get() to create
// the interface, which will invoke these methods
ifc_fn static iref<engine> get( const char* param ) {
//just create a new engine instance for the client
return new engine;
}
};
Note that if the host class is in a namespace, you have to use a fully qualified name when declaring the return type of interface creator methods.
In this example the client class will be defined in the “ifc/engine_interface.h” header file, generated by intergen. Apart from this file the generator also produces a cpp file with interface dispatcher. If the original header file where the host class was defined was “test.hpp”, then the generated dispatcher file will be named “test.intergen.cpp”.
To obtain the interface from client, include the interface header file and call:
iref<ot::engine_interface> ifc = ot::engine_interface::get("foo");
For javascript clients there are extra parameters in each creator method added by the intergen, so that one can specify the context where the script runs. The script_handle argument can be set up with a script string, or a *.js script file name, or even a html file or url of page that should be open and that will have bound the reference to the interface to a symbol name specified as the last parameter of the javascript creator function.
To map property access in Javascript to get/set methods in C++ one can decorate operator() in C++ class with ifc_fn
to get it function as property getter/setter for Javascript variable bound to an interface instance.
Two operators have to be provided: a setter with key and value-type arguments as a non-const method, and a getter returning the value-type, with one key-type parameter, as a const method.
class engine
{
public:
ifc_class(ot::engine_interface, ”ifc/”)
ifc_fn double operator()(const char* key) const;
ifc_fn void operator()(const char* key, double val);
};
This will allow you to use arbitrary member access in the script, for example:
ifc.key
ifc[“a string key”]
Note: the key argument can be anything with implicit conversion from const char* type, or coid::token or coid::charstr types.
With bidirectional interfaces, instances of host class can talk back to the connected clients via events. Events are class methods that are only declared inside the host class definition, and their definition is generated by intergen in the corresponding intergen.cpp file.
Bidirectional interfaces can be declared via ifc_class_var
macro, which also injects a member variable that will hold a reference to the connected client:
//host class
class engine
{
public:
ifc_class_var(ot::engine_interface, ”ifc/”, var)
//declaration only, the definition is generated automatically by intergen
ifc_event void update( float dt );
//use ifc_eventx(name) to have the interface event name different from
// the name of the method in the host class
ifc_eventx(init) void initialize();
};
Host instance can detect whether a client is connected by testing the value of variable declared with ifc_class_var
clause (var in the above example).
ifc_event
or ifc_eventx
can be then used to decorate a method declaration that can be called from host class and will be invoked in the derived client class. Note the body of the method will be generated by intergen and it therefore must not have definition in the host class itself.
ifc_volatile
can be used to mark event method parameters to indicate that the memory is temporarily mapped and valid for the duration of the call into clients. Javascript and Lua clients can optimize the way the arguments are passed, without duplicating arrays or strings.
ifc_evbody
can be used to specify the default implementation of an event, in case the client doesn't implement the event handling.
It can be specified after an ifc_event method declaration:
ifc_event bool event() const ifc_evbody(return false);
Interface can be declared virtual using ifc_class_virtual
decoration:
ifc_class_virtual(ot::object, "ifc/");
//normal ifc_fn/ifc_event methods
A virtual interface doesn't have creators, and can be used to provide an abstract base for other derived interfaces. A derived interface must implement all base interface methods. To declare a derived interface, use the following syntax:
ifc_class_var(ot::derived : ot::base, "ifc/")
//declare all base methods + any extra ones
WIP: Client context running in different thread, process or remotely Methods with no out-type arguments can be invoked asynchronously (blocking/non-blocking versions?) Methods marked thread-safe can be invoked in non-blocking mode from contexts in separate threads
The intergen parser recognizes the following constructs that help in controlling what should be included in the generated interface header files:
//ifc{
… code that should be shared with the interface class, will be copied into the
generated header file …
//}ifc
/*ifc{
… code that will be copied into the interface class header file, but is
inaccessible in the host class …
}ifc*/
class engine
{
...
To inject a code block specifically into the header class of a particular interface header, use the following construct:
//ifc{ ot::engine_interface
… code that should be shared with the interface class, will be copied into the
generated header file of ot::engine_interface only …
//}ifc
ifc_fnx(!)
or ifc_eventx(!)
can be used to suppress generating the method or the event in the scripting interface.
Apart from the C++ interface class, intergen also generates Javascript interface to the host class, which allows to access the host object from Javascript in the same way as from a C++ module.
To bind an interface to a Javascript object, call a creator in the generated JS wrapper object (in ifc/engine_interface.js.h):
iref<js::ot::engine_interface> iface = js::ot::engine_interface::get(script_handle(url,true));
The script_handle class accepts urls or direct scripts. The url can be either a javascript file (*.js), or a html/htm file that should be opened and where the interface object resides.
Html files can be open with optional attributes encoded as query tokens (after ?, separated by &). The following attributes are recognized:
name - window name width - initial window width height - initial window height x, y - initial window position transparent - 1/true, enable transparency conceal - 1/true, hide window from showing on screenshots
Since Javascript doesn’t support out or inout-type parameters, invocation of interface methods is a bit different from C++. In case there are multiple output-type parameters (including the return type if it’s not void), all out and inout values are returned encapsulated in a compound return value object as its members. The original return value is stored in $ret member in that case. If there is just one output value, the return value (or a single output parameter) is returned directly as the return value to Javascript.
Interface object can be created directly from the script by calling $query_interface injected Javascript function with the full interface name and creator function path as the first argument, followed by optional creator method arguments:
//for the ot::engine_interface as defined above, with creator fn get(const char* param)
var iobj = $query_interface(“ot::engine_interface.get”, “...”)
Sometimes it’s not desirable to expose all interface creator methods to Javascript, as they may be internal, requiring custom parameters that only make sense (or are available) on the C++ side. In that case, if the creator method begins with underscore character, it’s not visible to Javascript’s $query_interface call:
class engine
{
public:
ifc_class(ot::engine_interface, ”ifc/”)
//a creator method not visible from Javascript
ifc_fn static iref<engine> _get( const char* param );
//a hidden creator can be renamed so that the underscore doesn’t appear in clients
ifc_fnx(create) static iref<engine> _create();
};
- Right click on the project -> build customizations, add intergen props file (comm/intergen/intergen.props) and enable it
- Add a header file file into the project (can use hpp or hxx to differentiate from other headers), and click properties, and set the item type to intergen interface generator
- From that moment whenever the header file is modified, intergen runs and produces interface files, if it finds any interface declarations within the header
- Add the intergen generated *.intergen.cpp files into the project. If you add the *.js.intergen.cpp" file as well for Javascript interface support, you'll need to include v8.lib into link dependencies (or any other lib/dll that includes it)