A serialized COM server


This example illustrates how we can user serialized R objects to construct R COM servers and also how we can execute an R script each time an instance of an R COM server is created. Rather than defining a COM class definition such as a generator function to create methods, or a simple list of functions, it is often convenient to have a particular fixed S object that is used each time an instance of the COM server is needed. For example, suppose we have a particular dataset and we want to allow clients to have access to it as a COM object. It is most direct to simply serialize that dataset in an R data file (.rda) and instruct R to create the COM instances by deserializing that object and constructing a COM object directly from it. When defining the COM object, we need only serialize the S object and ensure that there is a method for the createCOMObject. Alternative approaches involve arranging to create unique instances of closures that have access to a copy of the data and, while possible, this can be more awkward and indirect.

The author of the COM server saves an R object to a file so that it is available to future R sessions. Then, we define a COM server definition that knows where to find this file containing the serialized data and uses it to construct the object. Let's start by creating a simple S object that we want to provide as a COM server. We'll create a simple matrix with the numbers 1 through 35 and with dimensions 5 by 7. Then we save it to the file <file>matrix.rda</file> (in the current directory).
m = matrix(1:35, 5, 7)
fn = paste(getwd(), "matrix.rda", sep=.Platform$file.sep)
save(m, file = fn)
Note that we can save any S object(s) at this point in the rda file.

Now we can proceed to register this fixed S object as a COM server. We provide a UUID for the class, either by selecting a specific one or generating it anew. And then we directly add the entry to the Windows registry so that clients can find and instantiate the server.
library(RDCOMServer)
clsid = getuuid("E9A94B2B-47C6-4CBA-D9A8-737577914171")	
registerClassID("Test.LocalMatrix", clsid, rda = fn, 
                profile = gsub("/", "\\\\", system.file("tests", "profile.S", package = "RDCOMServer")),
                testKey = "my own value")
What we have done here is put an entry into the registry under HKEY_CLASSES_ROOT for the program name Test.LocalMatrix. Under this, we have added the value of the UUID so that the COM mechanism can then jump in the registry to the actual definition. This is stored in HKEY_CLASSES_ROOT\{E9A94B2B-47C6-4CBA-D9A8-737577914171}. This key contains several entries, specifically rda, profile, and testKey.

The rda gives the name of the file into which we serialized the S value. This will be deserialized and converted to a COM object via a call to createCOMObject. The profile value here gives the name of a file containing S source code. This is source'ed each time an instance of this server is needed. This can be used to do initialization of the system such as loading packages, etc. that provid functions and methods that will be used by the resulting COM server.

Note that we don't need to provide any additional information in this particular case. The GetCOMClassDef function will be called with the UUID of this class and it will look for these keys in the associated registry key. On finding them, it will source the file identified by profile and deserialize the rda file. Then it calls createCOMObject on the resulting object obtained from the deserialization. In our case, we have a built-in method for handling matrices. If we needed to provide a method to handle the particular type, we could add it to the rda file, or to the code in the profile file.

These two keys provide sufficient flexibility to create a COM server from a fixed object. However, it is sometimes useful for the resulting COM server to be able to be parameterized by additional values. We can do this by putting additional entries into the UUID's registry key which the createCOMObject method can query to customize the serve. As a slightly unrealistic example, suppose we wanted to provide access to only a fixed set of columns in a very large data frame. Rather than having a copy of the subset on disk, we might want to add an additional property to the COM server which restricted the columns to the specified subset. While we can write a functional server to do this, we can also specify the serialized file and a registry entry, say columns, that gives a comma-separated list of the column names to export.

Given the registration above, a client can use this in much the same way it would use a regular COM object from S. For examle, in Python, we can create an instance of the server and ask for its dimensions.
from win32com.client import Dispatch
o = Dispatch("Test.LocalMatrix")
d = o.dim()
print d
Now we create a second instance to see if the profile is run a second time.
o = Dispatch("Test.LocalMatrix")
d = o.dim()
print d

Data Frames

In this example, we illustrate a slightly different variant of the same idea. Again we use a fixed S object; in this case the mtcars data frame. There is a method for createCOMObject for objects of this class (data.frame) so we can convert it to an COM object quite easily. The only thing that remains is to specify some code to ensure that the data is loaded into R and that we can tell R to use this as the data for the COM object. Loading the data into R we can do with the profile key. The code we want is the single expression
 data(mtcars)
Rather than going to the trouble of putting this in a file and registering the name of the file as the value for the profile key, it is more convenient to specify this command directl as the value of the key. Then, we arrange for the COMGetClassDef to interpret this value as a command if there is no file of that name. So now we save ourselves some additional hassle by giving the command inline.

If we were to copy the mechanism for the matrix in the example above, we would also have to specify the location of the file containing the serialized mtcars data frame. Again, it is more convenient to tell GetCOMClassDef to fetch the variable by name and not have to find the object indirectly via file. To do this, we specify the name of the S variable to use to get the desired object. To do this, we use the Svariable key and assign it a value of "mtcars". We don't provide a value for the rda key and GetCOMClassDef realizes what to do.

So now we know how to register such a server quite simply. The following code is very similar to that for the matrix example above, but differs only in that we provide an S command for the value of profile and the name "mtcars" for Svariable.
library(RDCOMServer)
clsid = getuuid("52d4f6bc-dd1c-45b3-0287-e469ede627f7")
registerClassID("S.Mtcars", clsid, 
                profile = "data(mtcars)",
                Svariable="mtcars")
Again, a simple Perl client illustrates the use of this server. Note that, as we would expect, the client doesn't need to know how the server is speicfied and created. It just gets a handle to the object and can start invoking methods.

use Win32::OLE;

$mtcars = Win32::OLE->new("S.Mtcars");

@dim= @{$mtcars->dim()};
print "Cars: ", $mtcars, "\n";

print $#dim, "\n";
print $dim[0], "x ", $dim[1], "\n";