2007-11-04 03:57:33 +00:00
|
|
|
synapse code design documentation
|
|
|
|
=================================
|
|
|
|
|
|
|
|
Objective:
|
|
|
|
----------
|
|
|
|
|
|
|
|
Provide a simple cross platform layer to use dynamically loaded code
|
|
|
|
inside a core application. Portability intended to win32 / linux / MacOS (?)
|
|
|
|
|
|
|
|
Main features are:
|
|
|
|
|
|
|
|
- designed for single process only, no remote clients, no asynchronous processes
|
|
|
|
- a client/server architecture, based on configuration files: a main binary,
|
|
|
|
loading a set of shared objects
|
|
|
|
|
|
|
|
Constraints:
|
|
|
|
------------
|
|
|
|
|
|
|
|
- large existing plugin code in Radiant!
|
|
|
|
must be compatible with minimal changes, specially for plugins (i.e. clients)
|
|
|
|
|
|
|
|
- make things as much transparent as possible
|
|
|
|
(ideally, no real difference between a static linkage and dynamic linkage,
|
|
|
|
cf usage of #define macros to wrap a function call onto a code pointer)
|
|
|
|
|
|
|
|
Features:
|
|
|
|
---------
|
|
|
|
|
|
|
|
Gather as much generic code as possible in a static .lib with minimal dependencies
|
|
|
|
(only dependency should be configuration files parser)
|
|
|
|
|
|
|
|
NOTE: current effective dependency is STL / glib / xml
|
|
|
|
|
|
|
|
Main executable implemented as a server, all others as clients. What has to
|
|
|
|
be done for a server / what has to be done for a client needs to be documented.
|
|
|
|
Provide as much scripts and tools and guidelines as needed (scripted generation of
|
|
|
|
some .h files?)
|
|
|
|
|
|
|
|
Proposed implementation:
|
|
|
|
------------------------
|
|
|
|
|
|
|
|
- have linux/ and win32/ subdirectories with OS-specific implementations
|
|
|
|
(such as dynamically loading shared objects, and doing the initial query?)
|
|
|
|
|
|
|
|
- reduce the API of a client to the minimum: one exported function?
|
|
|
|
provide a squeleton to make new clients easily?
|
|
|
|
|
|
|
|
Server use case:
|
|
|
|
1) build information about location of the modules (from code and config files)
|
|
|
|
2) load all modules and query information about their APIs
|
|
|
|
NOTE: could read the APIs from some XML description files instead of
|
|
|
|
querying it from the modules?
|
|
|
|
3) build information about the required function tables
|
|
|
|
i.e.: setup a list with the function tables to be filled in, and what they
|
|
|
|
need to be filled in with.
|
|
|
|
4) resolve the function table
|
|
|
|
NOTE: is this iterative? will some plugins request more APIs as they get filled
|
|
|
|
up?
|
|
|
|
NOTE: do we have optional tables?
|
|
|
|
5) unload unreferences modules
|
|
|
|
NOTE: we don't expect to be unloading a LOT of modules, otherwise we would
|
|
|
|
setup a solution that allows exploring of the APIs a given module provides
|
|
|
|
from a file description. Or you could 'cache' that (md5-checksum the file, and
|
|
|
|
maintain an XML list).
|
|
|
|
|
|
|
|
Client use case:
|
|
|
|
1) dynamically loaded
|
|
|
|
2) prompted for the interfaces it provides
|
|
|
|
2) prompted for the interfaces it requires
|
|
|
|
3) either unloaded, or told what interfaces have been filled in
|
|
|
|
|
|
|
|
The client module exports an Synapse_EnumerateInterfaces entry point
|
|
|
|
This returns an ISynapseClient, which lists what the plugin provides, and what it requires
|
|
|
|
|
|
|
|
The APIs:
|
|
|
|
|
|
|
|
An interface is a function table, GUID / major string / minor string
|
|
|
|
GUID is a shortcut to reference a major string (i.e. the human readable thing)
|
|
|
|
the GUID / major string is unique for a given interface
|
|
|
|
minor string is used to reference a particular version of an API
|
|
|
|
(for instance when talking about image loading functionality, tga and jpg etc.)
|
|
|
|
|
|
|
|
The GUID scheme is handy because it provides easy tests. They are not strictly
|
|
|
|
necessary but we will probably want to keep them. Should we extend to GUIDs
|
|
|
|
for minor too?
|
|
|
|
|
|
|
|
Roadmap:
|
|
|
|
--------
|
|
|
|
|
|
|
|
Need to convert the core (as server) and the required modules. Will have
|
|
|
|
clearer view of what's to be done along the way.
|
|
|
|
|
|
|
|
Implementation design:
|
|
|
|
----------------------
|
|
|
|
|
|
|
|
There is a client and server side to synapse. Typically server is in Radiant or q3map,
|
|
|
|
client is in any module. For implementation, we have one server class and one client class.
|
|
|
|
It would be possible to have two seperate libraries, synapse-client and synapse-server. But
|
|
|
|
that only brings down the statically linked stuff to make things more complicated build-sysem
|
|
|
|
wise.
|
|
|
|
|
|
|
|
Initial implementation has been using isynapse.h and synapse.h, to provide a pure virtual
|
|
|
|
base class for server and client. But that doesn't bring any major functionality, it's easier
|
|
|
|
if both sides see the full API of the client and server classes.
|
|
|
|
|
|
|
|
A side problem is the diagnostic printing functionality. For easy debugging we require that
|
|
|
|
the synapse code can have access to a Sys_Printf or similar function at all times (that is for
|
|
|
|
client and server implementation). On client we will pipe through the main API to the server
|
|
|
|
as soon as we can in most cases. Using Sys_Printf would bring us to a dead end situation, since
|
|
|
|
when synapse is used as the server, the main code implements it's own Sys_Printf stuff.
|
|
|
|
Instead we introduce a local Syn_Printf implementation, which can be overriden in the server
|
|
|
|
to point to the appropriate print functions.
|
|
|
|
|
|
|
|
Runtime config:
|
|
|
|
---------------
|
|
|
|
|
|
|
|
Something that has not been looked upon a lot yet, runtime configuration. What interfaces
|
|
|
|
are loaded etc. Ideally, from an XML config file. A client explicitely requests the
|
|
|
|
server to load all the interfaces it requires (in this case, the client is radiant or
|
|
|
|
q3map).
|
|
|
|
|
|
|
|
Plugins are somewhat out of the 'required interfaces' frame, since they are loaded
|
|
|
|
whenever they are found. It is possible however that some plugins would not want to be
|
|
|
|
loaded in if the game doesn't match etc. in case they would need to access the global
|
|
|
|
config?
|
|
|
|
|
|
|
|
In most cases a given API is only required once for editor functionality. (VFS for
|
|
|
|
instance), so our #define strategy for easy mapping of the functions should still work.
|
|
|
|
|
|
|
|
Version checks, reference counting:
|
|
|
|
------------------------------------
|
|
|
|
|
|
|
|
Need version checking at several levels. A version string (major/minor) on the main API
|
|
|
|
entry point (straight in the exported function to save as much as possible for
|
|
|
|
compatibility). For individual APIs, we have been feeding the struct size into the first
|
|
|
|
int of the struct so far, and it has worked very well.
|
|
|
|
|
|
|
|
Reference counting: we introduced class based APIs to solve the ref counting issues,
|
|
|
|
which are not easy to solve on C function tables. That problem would arise in plugin
|
|
|
|
situations where we might want to 'reload' or 'unload' some plugins. The server could
|
|
|
|
keep track of the ref count.
|
|
|
|
|
|
|
|
Caching?
|
|
|
|
--------
|
|
|
|
|
|
|
|
We are going to load every shared object we find and query it for it's interfaces. Then
|
|
|
|
we will unload the stuff we don't want. This is going to slow down the startup process.
|
|
|
|
We could extract the API information in a cache to avoid the loading step.
|
|
|
|
|
|
|
|
Interfaces with multiple minors against I* objects?
|
|
|
|
---------------------------------------------------
|
|
|
|
|
|
|
|
Looking at the iimage.h API, why not having instead something that enumerates C++ objects
|
|
|
|
directly? Mainly because we want to be able to spread several minors accross multiple modules
|
|
|
|
and still use them together. And straight laid out function tables in C structs are only
|
|
|
|
one indirection when the table is static.
|
|
|
|
|
|
|
|
This raises a broader topic, instead of requesting APIs, we could request objects directly.
|
|
|
|
Would that be of any use?
|
|
|
|
|
|
|
|
Loading interfaces / resolving interdependencies strategy
|
|
|
|
---------------------------------------------------------
|
|
|
|
|
|
|
|
Some notes about how we load the modules and resolve interdependencies:
|
|
|
|
|
|
|
|
We want to avoid requesting a module for an API it provides before all the APIs it requires
|
|
|
|
have been filled in (mostly stability concerns, a module may be doing whatever internally
|
|
|
|
when we request something from it). The exception being the module we are trying to resolve
|
|
|
|
for (since we need a start point for resolution). But in all likelyness we resolve for radiant
|
|
|
|
or q3map for instance.
|
|
|
|
|
|
|
|
With this approach, it is possible that some situations could not be resolved, for instance:
|
|
|
|
Radiant
|
|
|
|
requires A
|
|
|
|
provides B
|
|
|
|
module 1
|
|
|
|
requires C
|
|
|
|
provides A
|
|
|
|
module 2
|
|
|
|
requires A
|
|
|
|
provides C
|
|
|
|
if we start by resolving Radiant, we will get stuck
|
|
|
|
if we are ready to ask module to provide the API even though the required is not meant, it would work
|
|
|
|
but that kind of situation is very unlikely, so sticking to safer strategy
|
|
|
|
|
|
|
|
Configuration
|
|
|
|
-------------
|
|
|
|
|
|
|
|
the config info needs to go down to the clients too
|
2008-08-24 03:20:51 +00:00
|
|
|
for instance, mapxml loaded for q3map or radiant, doesn't rely on the same major?
|