gtkradiant/libs/synapse/doc/design.txt

190 lines
8 KiB
Text
Raw Normal View History

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
for instance, mapxml loaded for q3map or radiant, doesn't rely on the same major?