Module (TinyMUX)

From TinyMUX
Jump to navigation Jump to search

Overview

A module does not necessarily need to be written using the same programming language as netmux. As long as that language supports an array of function pointers, it can provide and consume module interfaces.

Modules should be able to provide binary compatibility across a single box/platform. No module interface should change in any case (signatures of its methods or semantics of its use), but as long as libmux doesn't change in an incompatible way, binary compatibility is achieved.

It isn't even necessary for the module to be hosted by netmux. A compiled module could be used as-is with another MU server or even a tic-tac-toe game as long as the module interfaces are satisfied. It is even possible for someone to distribute binary-only modules with the expectation that they would work without change with later versions of TinyMUX.

Tested platforms include BSD, Linux, OS X, and Windows in both 32-bit and 64-bit flavors. On OS X, the module must be a dynalib. On Windows, the module and libmux must be DLLs.

In-Process

Loading a Module

Loading a module (e.g., sample.so) starts with netmux (which loads libmux.so automatically). Libmux.so is then used to load sample.so explicitly.

Since netmux and sample.so are both compiled using -lmux, they have a run-time load dependency on libmux.so. It isn't necessary for netmux or sample.so to explicitly load libmux.so. This happens as part of running netmux or dynamically loading sample.so. In fact, on OS X, the sample module is compiled into sample.dynalib as that platform emphasizes this difference in the filename.

As a host, netmux begins its conversation with libmux.so simply enough. As shown above, mux_InitModuleLibrary() is the first call to libmux.so, and as shown below, mux_FinalizeModuleLibrary() is the last call to libmux.so. The other four functions allow netmux to manage the set of loaded modules.

Unloading a Module

While netmux begins the business of unloading a module, libmux.so first asks the module to revoke its interfaces. It then unloads sample.so. Note that while revoking its interfaces does prevent any new instances from being created, if there are live component instances that depend on the module, the module is not unloaded immediately. Periodically, mux_ModuleMaintenance() is called. When all the instances are release, the module will finally and completely unload.

MUX_RESULT mux_InitModuleLibrary(process_context ctx, PipePump *fpPipePump, QUEUE_INFO *pQueue_In, QUEUE_INFO *pQueue_Out);
MUX_RESULT mux_FinalizeModuleLibrary(void);

MUX_RESULT mux_AddModule(const UTF8 aModuleName[], const UTF8 aFileName[]);
MUX_RESULT mux_RemoveModule(const UTF8 aModuleName[]);
MUX_RESULT mux_ModuleInfo(int iModule, MUX_MODULE_INFO *pModuleInfo);
MUX_RESULT mux_ModuleMaintenance(void);

As a provider and consumer of module interfaces, both netmux and sample.so use the additional three calls into libmux.so:

MUX_RESULT mux_CreateInstance(MUX_CID cid, mux_IUnknown *pUnknownOuter, create_context ctx, MUX_IID iid, void **ppv);
MUX_RESULT mux_RegisterClassObjects(int nci, CLASS_INFO aci[], FPGETCLASSOBJECT *pfGetClassObject);
MUX_RESULT mux_RevokeClassObjects(int nci, CLASS_INFO aci[]);

There is no need to use the following calls, yet. They will be used in conjunction with Standard Marshaling which is not fully implemented. Standard Marshaling requires an associated proxy-stub component. Custom Marshaling uses a different mechanism, and in-proc modules do not need their interfaces to be marshaled at all.

MUX_RESULT mux_RegisterInterfaces(int nii, INTERFACE_INFO aii[]);
MUX_RESULT mux_RevokeInterfaces(int nii, INTERFACE_INFO aii[]);

In response to a mux_AddModule() request, libmux.so loads the external module. It expects to find the following four functions exported by the module:

MUX_RESULT mux_CanUnloadNow(void);
MUX_RESULT mux_GetClassObject(UINT64 cid, UINT64 iid, void **ppv);
MUX_RESULT mux_Register(void);
MUX_RESULT mux_Unregister(void);

After loading the module, libmux.so calls mux_Register() in the module. This should cause a call back into libmux.so to mux_RegisterClassObjects() from the module. The end result of this initialization is that libmux.so has a complete registry of components provided by netmux and sample.so. Also, it is able to make those available to both sides via mux_CreateInstance(). This follows the following sequence:

  • Either netmux or sample.so calls mux_CreateInstance(cid, NULL, ctx, iid, ppv) to request a specific interface (iid) from a specific component (cid).
  • libmux.so determines from its registry of all modules to call mux_GetClassObject(cid, iid, ppv) in a particular module to get a IClassFactory interface capable of creating the requested interface on the requested component.
  • The Class Factory is then called via its MUX_RESULT CreateInstance(NULL, iid, ppv) method to obtain the desired interface which is returned to the original caller.
  • The Class Factory is then released by calling its Release() method.
  • When the requested interface is no longer wanted, it is also released by calling its Release() method.

A module interface is just an array of function pointers. Allocation and deallocation of memory associated with an instance is performed by the binary that implements the interface. Module interfaces are reference-counted. When the last reference is released with a call to the Release() method, the object destroys itself.

Registering

When netmux or a module registers the components it supports, it is in effect advertising them. In the call to mux_RegisterClassObjects(), acid and ncid are an array and count of the UINT64 component ids (cids) that are provided by the caller. The compilation of these component ids in libmux corresponds to part of the registery in COM.

Containment

Containment is exposing another component's interface(s) as if it(they) were your own.

Implementing containment correctly is not easy. In fact, unless you really know what you're doing, don't try. Just pass NULL for pUnknownOuter when you create, and check that pUnknownOuter is NULL in your Class Factory. I wanted to remove the ability, but Standard Marshaling uses it on the proxy side. The scissors are sharp.

mux_IUnknown

interface mux_IUnknown
{
public:
    virtual MUX_RESULT QueryInterface(UINT64 iid, void **ppv) = 0;
    virtual UINT32     AddRef(void) = 0;
    virtual UINT32     Release(void) = 0;
};

mux_IClassFactory

interface mux_IClassFactory : public mux_IUnknown
{
public:
    virtual MUX_RESULT CreateInstance(mux_IUnknown *pUnknownOuter, UINT64 iid, void **ppv) = 0;
    virtual MUX_RESULT LockServer(bool bLock) = 0;
};

MUX_RESULT

This corresponds to COM's HRESULT. There are two success cases (MUX_S_OK and MUX_S_FALSE) and many failure cases.

typedef int MUX_RESULT;

#define MUX_S_OK                 (0)
#define MUX_S_FALSE              (1)
#define MUX_E_FAIL              (-1)
#define MUX_E_OUTOFMEMORY       (-2)
#define MUX_E_CLASSNOTAVAILABLE (-3)
#define MUX_E_NOINTERFACE       (-4)
#define MUX_E_NOTIMPLEMENTED    (-5)
#define MUX_E_INVALIDARG        (-6)
#define MUX_E_UNEXPECTED        (-7)
#define MUX_E_NOTREADY          (-8)
#define MUX_E_NOTFOUND          (-9)
#define MUX_E_NOAGGREGATION     (-10)

cid

A Component ID is a UINT64 number which uniquely names a component. Multiple instances of the same component can be created. A component usually encapsulated some state which in turn can be manipulated by one or more interfaces. A component can support multiple interfaces simultaneously, and the interfaces do not necessarily expose all of the component's state.

Because a Component ID uniquely names the the component, it is also called a Class ID. That is, it is the plan from which one more instances are created. To use a fruit analogy, a CID corresponds to the name of a fruit (Apple, Orange, etc.). Creating a component of a particular CID is like picking an Apple off the tree. It is an Apple like all the other apples on the tree, but you also have an apple and not any other apple.

By convention the 64-bit CID is divided into an upper, 32-bit part and a lower, 32-bit part. A CID of zero is reserved. A CID with an upper part of zero is reserved. A CID with an upper part of 0x00000001 is reserved by libmux. A CID with an upper part of 0x00000002 is reserved by the hosting server (netmux, netmush, etc.). A CID with a lower part of zero is reserved. To reduce the chance of conflicts, all component authors should claim for themselves an upper value (in whatever fashion), and reuse this value for all components they author.

iid

Interface ID is a UINT64 number which uniquely names an interface.

By convention the 64-bit IID is divided into an upper, 32-bit part and a lower, 32-bit part. An IID of zero is reserved. An IID with an upper part of zero is reserved. A IID with an upper part of 0x00000001 is reserved by libmux. An IID with an upper part of 0x00000002 is reserved by the hosting server (netmux, netmush, etc.). An IID with a lower part of zero is reserved. To reduce the chance of conflicts, all interface authors should claim for themselves an upper value (in whatever fashion), and reuse this value for all interfaces they author.

process_ctx

typedef enum
{
    IsUninitialized  = 0,
    IsMainProcess    = 1,
    IsSlaveProcess   = 2
} process_context;

create_context

typedef enum
{
    UseSameProcess  = 1,
    UseMainProcess  = 2,
    UseSlaveProcess = 3,
    UseAnyContexts  = 7
} create_context;

Example

// Use of CLog provided by netmux.
//
mux_ILog *pILog = NULL;
MUX_RESULT mr = mux_CreateInstance(CID_Log, NULL, UseSameProcess, IID_ILog, (void **)&pILog);
if (MUX_SUCCEEDED(mr))
{
#define LOG_ALWAYS      0x80000000  /* Always log it */
    if (pILog->start_log(LOG_ALWAYS, T("INI"), T("INFO")))
    {
        pILog->log_printf("Sample module registered.");
        pILog->end_log();
    }

    pILog->Release();
    pILog = NULL;
}

Out-Of-Process

Marshaling across two processes is accomplished using the in-process methods described above with the addition of a transport between the two sides. This transport consists of an incoming byte stream, and outgoing byte stream, and a routine to drive the pumping. libmux is not aware of Unix pipes, sockets, or any other details of transport method. Those details are the responsibility of the hosting process.

The transport is then channelized to create the necessary association between caller and callee. Marshaling an interface is then the same thing as creating a new channel. Currently, only Custom Marshalling is implemented, but Standard Marshalling is part of the design. To use Custom Marshaling, both the component with the interface to be marshaled as well as the in-process proxy must implement the mux_IMarshal interface described below. On the component side, the interface is marshaled into a marshal packet. On the proxy side, the marshal packet is turned again into an interface.

interface mux_IMarshal : public mux_IUnknown
{
public:
    virtual MUX_RESULT GetUnmarshalClass(MUX_IID riid, marshal_context ctx, MUX_CID *pcid) = 0;
    virtual MUX_RESULT MarshalInterface(QUEUE_INFO *pqi, MUX_IID riid, marshal_context ctx) = 0;
    virtual MUX_RESULT UnmarshalInterface(QUEUE_INFO *pqi, MUX_IID riid, void **ppv) = 0;
    virtual MUX_RESULT ReleaseMarshalData(QUEUE_INFO *pqi) = 0;
    virtual MUX_RESULT DisconnectObject(void) = 0;
};

marshall_context

This becomes meaningful when the module interface must be marshalled.

typedef enum
{
    CrossProcess = 0,
    CrossThread  = 1
} marshal_context;

Interface Marshaling Helpers

Rather than deal with mux_IMarshal interfaces directly, the following two helper functions simplify the process of turning an interface into a marshaling packet.

MUX_RESULT mux_MarshalInterface(QUEUE_INFO *pqi, MUX_IID riid, mux_IUnknown *pIUnknown, marshal_context ctx);
MUX_RESULT mux_UnmarshalInterface(QUEUE_INFO *pqi, MUX_IID riid, void **ppv);

Queues and Pump

typedef MUX_RESULT FCALL(struct channel_info *pci, QUEUE_INFO *pqi);
typedef MUX_RESULT FMSG(struct channel_info *pci, QUEUE_INFO *pqi);
typedef MUX_RESULT FDISC(struct channel_info *pci, QUEUE_INFO *pqi);

typedef struct channel_info
{
     bool      bAllocated;
     UINT32    nChannel;
     FCALL    *pfCall;
     FMSG     *pfMsg;
     FDISC    *pfDisc;
     void     *pInterface;
} CHANNEL_INFO, *PCHANNEL_INFO;

PCHANNEL_INFO Pipe_AllocateChannel(FCALL *pfCall, FMSG *pfMsg, FDISC *pfDisc);
void          Pipe_AppendBytes(QUEUE_INFO *pqi, size_t n, const void *p);
void          Pipe_AppendQueue(QUEUE_INFO *pqiOut, QUEUE_INFO *pqiIn);
bool          Pipe_DecodeFrames(UINT32 nReturnChannel, QUEUE_INFO *pqiFrame);
void          Pipe_EmptyQueue(QUEUE_INFO *pqi);
PCHANNEL_INFO Pipe_FindChannel(UINT32 nChannel);
void          Pipe_FreeChannel(CHANNEL_INFO *pci);
bool          Pipe_GetByte(QUEUE_INFO *pqi, UINT8 ach[1]);
bool          Pipe_GetBytes(QUEUE_INFO *pqi, size_t *pn, void *pch);
void          Pipe_InitializeQueueInfo(QUEUE_INFO *pqi);
size_t        Pipe_QueueLength(QUEUE_INFO *pqi);
MUX_RESULT    Pipe_SendCallPacketAndWait(UINT32 nChannel, QUEUE_INFO *pqi);
MUX_RESULT    Pipe_SendMsgPacket(UINT32 nChannel, QUEUE_INFO *pqi);
MUX_RESULT    Pipe_SendDiscPacket(UINT32 nChannel, QUEUE_INFO *pqi);

Server-Provided Interfaces

mux_ILog

mux_ILog is provided by netmux (CID_Log) to give access to the server's log file.

interface mux_ILog : public mux_IUnknown
{
public:
    virtual bool start_log(int key, const UTF8 *primary, const UTF8 *secondary) = 0;

    virtual void log_perror(const UTF8 *primary, const UTF8 *secondary, const UTF8 *extra, const UTF8 *failing_object) = 0;
    virtual void log_text(const UTF8 *text) = 0;
    virtual void log_number(int num) = 0;
    virtual void DCL_CDECL log_printf(const char *fmt, ...) = 0;
    virtual void log_name(dbref target) = 0;
    virtual void log_name_and_loc(dbref player) = 0;
    virtual void log_type_and_name(dbref thing) = 0;

    virtual void end_log(void) = 0;
};

mux_IServerEventsSink

mux_IServerEventsSink is provided by modules to accept broadcast server events.

interface mux_IServerEventsSink : public mux_IUnknown
{
public:
    virtual void startup(void) = 0;
    virtual void presync_database(void) = 0;
    virtual void presync_database_sigsegv(void) = 0;
    virtual void dump_database(int dump_type) = 0;
    virtual void dump_complete_signal(void) = 0;
    virtual void shutdown(void) = 0;
    virtual void dbck(void) = 0;
    virtual void connect(dbref player, int isnew, int num) = 0;
    virtual void disconnect(dbref player, int num) = 0;
    virtual void data_create(dbref object) = 0;
    virtual void data_clone(dbref clone, dbref source) = 0;
    virtual void data_free(dbref object) = 0;
};

mux_IServerEventsControl

mux_IServerEventsControl is provided by netmux (CID_ServerEventsSource) to accept mux_IServerEventsSink interfaces and broadcast server events.

interface mux_IServerEventsControl : public mux_IUnknown
{
public:
    virtual MUX_RESULT Advise(mux_IServerEventsSink *pIServerEvents) = 0;
};

mux_ISlaveControl

mux_ISserverSlaveControl is provided by the stubslave (CID_StubSlave) to allow the main server to manage the stubslave's use of libmux and tell it when to shutdown.

interface mux_ISlaveControl : public mux_IUnknown
{
public:
#ifdef WIN32
    virtual MUX_RESULT AddModule(const UTF8 aModuleName[], const UTF16 aFileName[]) = 0;
#else
    virtual MUX_RESULT AddModule(const UTF8 aModuleName[], const UTF8 aFileName[]) = 0;
#endif // WIN32
    virtual MUX_RESULT RemoveModule(const UTF8 aModuleName[]) = 0;
    virtual MUX_RESULT ModuleInfo(int iModule, MUX_MODULE_INFO *pModuleInfo) = 0;
    virtual MUX_RESULT ModuleMaintenance(void) = 0;
    virtual MUX_RESULT ShutdownSlave(void) = 0;
};

SQLSlave/SQLProxy Interfaces

AsyncSQL requires two modules: sqlslave and sqlproxy.

mux_IQuerySink

mux_IQuerySink is provided by netmux (CID_QueryClient) to receive result sets from queries.

interface mux_IQuerySink : public mux_IUnknown
{
public:
    virtual MUX_RESULT Result(UINT32 iQueryHandle, const UTF8 *pResultSet) = 0;
};

mux_IQueryControl

mux_IQueryControl is provided by the external sqlslave module (CID_QueryServer) to manage the conversation with MySQL and make queries.

interface mux_IQueryControl : public mux_IUnknown
{
public:
    virtual MUX_RESULT Connect(const UTF8 *pServer, const UTF8 *pDatabase, const UTF8 *pUser, const UTF8 *pPassword) = 0;
    virtual MUX_RESULT Advise(mux_IQuerySink *pIQuerySink) = 0;
    virtual MUX_RESULT Query(UINT32 iQueryHandle, const UTF8 *pDatabaseName, const UTF8 *pQuery) = 0;
};