One important benefit of data coupling objects through messaging is the ability to use objects in other processes or on other computers. This tutorial demonstrates how object servers are created and accessed in casycom through a socket.

The first question to consider is how object servers are found and launched. Other COM frameworks typically solve this through a specialized service that does just that. DBUS, for example, has the dbus-daemon service through which all messages must be routed and which will launch registered object servers as needed. Casycom does not use a central message hub service like this (although one could be implemented), but instead expects applications to connect to object servers through well-known sockets. The servers themselves can either be started at boot time, launched by the applications that need them, or by using systemd socket activation. This last method is the easiest and the preferred one. Systemd is already adopted by a majority of Linux distributions and some of the remainder also use an init that can do socket activation. Lacking that, a standard launching service could be implemented to do this.

Client Side

Interprocess communication in casycom needs to be enabled explicitly, because over three quarters of the library is pulled in when it is used. Sometimes it is useful to just use a local object message loop, and get it for a very low cost of about 3k of object code linked in. So, to begin using external objects, casycom_enable_externs() needs to be called, likely in App_Init.

Connections to external objects are managed by an Extern object, of which one is created per socket. On the client side, it is sufficient. On the server side, the socket is being listened on will generate the actual connection sockets, so a separate object, ExternServer, is needed to keep track of them and to listen for clients on the socket.

const iid_t eil_Ping[] = { &i_Ping, NULL };

void App_App_Init (App* app, unsigned argc, char* const* argv)
{
    casycom_enable_externs();
    app->externp = casycom_create_proxy (&i_Extern, oid_App);
    if (0 > PExtern_ConnectUserLocal (&app->externp, "ipcom.socket", eil_Ping))
	casycom_error ("Extern_ConnectUserLocal: %s", strerror(errno));
}

The client side creates a proxy to an Extern object and asks it to connect to a specific socket, here "ipcom.socket". PExtern_ConnectUserLocal will connect to the socket in the user's runtime directory, the standard for user socket location, obtained from the environment variable XDG_RUNTIME_DIR, typically set to /run/user/<uid>. Other connect call variants can be found in xcom.h.

The third argument to _Connect is the NULL-terminated list of imported interfaces that will be created on the other side of the socket. Here the list contains only the Ping interface. Note that the factory for it is not registered - that will be done on the server side.

The _Connect function returns the file descriptor of the connected socket. If the socket can not be opened, -1 will be returned, and errno will be set appropriately. Errors in casycom should be reported by calling casycom_error, a printf-like call that sets the error string. Error handling will begin as soon as the failing function returns, and works somewhat like exception propagation. The object originating the message routed to the failed function will be contacted and its .Error function pointer (in the Factory) will be called, if present. If the error handler returns true, the error will be considered handled and the execution will resume normally. Otherwise, the handling object is considered failed as well, and its creator object will be given the chance to handle the error next. If nobody handles the error, it will be printed and the process will terminate.

If the error occurs in the remote object, in another process, the error will be forwarded to the object's caller, in the client process. On the server side, the error will be considered handled and it will be up to the client to decide what to do about it.

void App_ExternR_Connected (App* app, const ExternInfo* einfo)
{
    if (einfo->interfaces.size < 1 || einfo->interfaces.d[0] != &i_Ping)
	return casycom_error ("connected to server that does not support the Ping interface");
    app->pingp = casycom_create_proxy (&i_Ping, oid_App);
    PPing_Ping (&app->pingp, 1);
}

Extern connections are opened asynchronously, so the App must wait until the ExternR_Connected notification before any imported objects can be created. The notification contains an ExternInfo with a list of interfaces actually available on this connection, the credentials of the process on the other side, and a flag specifying whether it is on a UNIX domain socket, which indicates that credentials and file descriptors can be sent over it.

Once the App verifies that the Ping interface is indeed present, proxies to objects of that type can now be created and used in the same manner as they were for local objects, with the PingR reply arriving the same way. Sending Ping messages before the connected notification arrives results in an error.

void App_PingR_Ping (App* app, uint32_t u)
    { casycom_quit (EXIT_SUCCESS); }

void App_Error (void* vapp, oid_t eoid, const char* msg)
{
    casycom_log (LOG_ERR, "error from object %hu: %s", eoid, msg);
    return false;
}

void App_ObjectDestroyed (void* vapp, oid_t oid)
{
    printf ("Called object %hu has been destroyed\n", oid);
}

const DApp d_App_App = {
    .interface = &i_App,
    DMETHOD (App, App_Init)
};
const DPingR d_App_PingR = {
    .interface = &i_PingR,
    DMETHOD (App, PingR_Ping)
};
const DExternR d_App_ExternR = {
    .interface = &i_ExternR,
    DMETHOD (App, ExternR_Connected)
};
const Factory f_App = {
    .Create	= App_Create,
    .Destroy	= App_Destroy,
    .Error	= App_Error,
    .ObjectDestroyed = App_ObjectDestroyed,
    .dtable	= { &d_App_App, &d_App_PingR, &d_App_ExternR, NULL }
};
CASYCOM_MAIN (f_App)

Finishing off the client side, there are two additional Factory methods demonstrated. The .Error function has been explained above. .ObjectDestroyed is called on an object when it has a proxy to an object that has been deleted. Usually there would be no other notification that this has happened. The proxy, of course, is still valid after this, but messages sent through it will result in a new object being created at the other end of the link. ObjectDestroyed can thus be useful in situations when the calling object keeps pointers into the called one.

Server Side

The server side needs to bind to a socket and listen for incoming connections. The recommended way is to use an ExternServer to do these things as well as spawn an Extern object to manage each accepted connection.

const iid_t eil_Ping[] = { &i_Ping, NULL };

void App_App_Init (App* app, unsigned argc, char* const* argv)
{
    casycom_enable_externs();
    casycom_register (&f_ExternServer);
    casycom_register (&f_Ping);
    app->externp = casycom_create_proxy (&i_ExternServer, oid_App);
    if (sd_listen_fds())
	PExternServer_Open (&app->externp, SD_LISTEN_FDS_START+0, eil_Ping);
    else if (0 > PExternServer_BindUserLocal (&app->externp, "ipcom.socket", eil_Ping))
	casycom_error ("ExternServer_BindUserLocal: %s", strerror(errno));
}

Here the ExternServer and Ping interfaces are registered. It is recommended that systemd socket activation be supported. To do so, call sd_listen_fds to determine how many sockets have been passed from systemd. Although there may be more than one, this example will only bind to the first one. Supporting multiple fds will require an ExternServer object for each. The passed-in fds begin at SD_LISTEN_FDS_START.

If not launched through socket activation, "ipcom.socket" is created in the user's runtime directory in the same manner it was connected to in the client. Here the interface list contains exported interfaces; the server side does not normally import any.

ExternServer forwards the ExternR_Connected notifications to the App, requiring the latter to implement ExternR, but in this example the handler for that does nothing.

void ServerApp_App_Init (App* app, unsigned argc, char* const* argv)
{
    casycom_enable_externs();
    casycom_register (&f_Ping);
    app->externp = casycom_create_proxy (&i_Extern, oid_App);
    PExtern_Open (&app->externp, STDIN_FILENO, EXTERN_SERVER, NULL, eil_Ping);
}

void ClientApp_App_Init (App* app, unsigned argc, char* const* argv)
{
    casycom_enable_externs();
    app->externp = casycom_create_proxy (&i_Extern, oid_App);
    PExtern_LaunchPipe (&app->externp, "ipcom", "-p", eil_Ping);
}

There is also a more direct way to make an object server: by making just one connection and using one Extern object at each end. This method does not require creating a public socket, and so is appropriate for use with private object servers. The server uses PExtern_Open to attach to the socket on stdin, and the client uses PExtern_LaunchPipe to launch the server process and create the the socket pipe. The client side imports Ping, the server side exports it.