Libcom usage example

From LSDevLinux
Jump to: navigation, search

libcom provides an implementation of connecting Component Object Model (COM) [1][2] clients and servers on Linux, similar to ole32.dll on Windows.

.NET objects and COM components interact using Runtime Callable Wrappers (RCWs) and COM Callable Wrappers (CCWs). RCWs and CCWs are implemented in Mono.

This is an example of using libcom and Mono to:

  • Access a C++ COM server from C#
  • Access a C# COM server from a C++ COM server


Abstract

  1. Create a C# test program.
  2. Create a C# interface IJar and class Jar, with some COM-related attributes.
  3. Create a C++ COM server, Bakery.
  4. Create an IBakery.idl file.
  5. Windows$ midl /Oicf /env win32 /tlb IBakery.tlb /h IBakery_idl.h IBakery.idl
  6. Windows$ tlbimp IBakery.tlb /out:IBakeryMetadata.dll
  7. Tweak IBakery_idl.h with a script to make it compatible with libcom.
  8. Build the C# Jar to Jar.dll.
  9. Build the C# test program, referencing Jar.dll and IBakeryMetadata.dll.
  10. Windows$ tlbexp Jar.dll /out:Jar.tlb
  11. Create a one-line file, temp.cpp, with contents: #import "Jar.tlb"
  12. Windows$ cl /LD temp.cpp
  13. Copy the resulting jar.tlh,jar.tli to Linux.
  14. Tweak jar.tlh,jar.tli to make them compile.
  15. Bakery #includes jar.tlh.
  16. Build the C++ Bakery COM server to an .so.
  17. Run the C# test program.

Access C++ COM server from C#

To access a C++ COM server from C#, you will need an interop assembly file. Mono will use this when it creates an RCW to allow the C# code to transparently access the COM server.

To do this, do the following:

Create a C++ COM server

Creating IBakeryMetadata.dll, IBakery_idl.h, and IBakery_GUIDs.cpp

Create an IDL file:

// IBakery.idl
 
import "oaidl.idl";
import "ocidl.idl";
 
[uuid(af4e32b4-93ce-41fc-9ed7-c3b442313152), version(1.0)]
library IBakeryLib
{
 importlib("stdole32.tlb");
 importlib("stdole2.tlb");
 
 [object, uuid(cbdb8382-a142-489a-8831-2b60069d2841)]
 interface IBakery : IUnknown
 {

  [id(1)] 
  HRESULT getNumberCookiesBaked(
   [out, retval] long * number);

  [id(2)]
  HRESULT bakeCookies( 
   [in] long amount,
   [in] IUnknown * jar);
 }
 
 [uuid(01fe1db4-5cfc-482b-9aae-266d94f5ece8)]
 coclass Bakery
 {
  [default] interface IBakery;
 }
}

In Windows, process the IDL file into _idl.h and TLB files:

Windows$ midl /Oicf /env win32 /tlb IBakery.tlb /h IBakery_idl.h IBakery.idl

Also in Windows, process the TLB into an interop assembly:

Windows$ tlbimp IBakery.tlb /out:IBakeryMetadata.dll

Process the _idl.h file to make it compatible with libcom:

$ sed -e '
   /EXTERN_C const \(IID\|CLSID\|LIBID\|DIID\) \(IID\|CLSID\|LIBID\|DIID\)_\(..*\);/{
    s//#define \2_\3 __uuidof(\3)/
   }
  ' IBakery_idl-original.h > IBakery_idl.h

This changes things like

EXTERN_C const IID IID_IBakery;

to

#define IID_IBakery __uuidof(IBakery)

Generate GUIDs that will work without Microsoft's compiler:

echo "#include \"IBakery_idl.h\"" > IBakery_GUIDs.cpp
sed -n -e '
 /MIDL_INTERFACE("/{
  h
  n
  G
  s/[ \t]*:.*\n//
  s/^[ \t]*/template<> const GUID __uuidof(/
  s/[ \t]*MIDL_INTERFACE/)/
  s/$/;/
  p
 }
 /DECLSPEC_UUID("/{
  h
  n
  G
  s/^/template<> const GUID __uuidof(/
  s/class DECLSPEC_UUID/)/
  s/[ \t]*;.*\n//
  s/$/;/
  p
 }
 ' IBakery_idl.h >> IBakery_GUIDs.cpp

This produces a short file such as

#include "IBakery_idl.h"
template<> const GUID __uuidof(IBakery)("cbdb8382-a142-489a-8831-2b60069d2841");
template<> const GUID __uuidof(Bakery)("01fe1db4-5cfc-482b-9aae-266d94f5ece8");

Processing the _idl.h can be done automatically in the build process using scripts in our source repository.

Creating Bakery.h, Bakery.cpp

IBakery is defined in IBakery_idl.h. Make a C++ header file to define Bakery.

// Bakery.h

#include "IBakery_idl.h"
#include "COMLibrary.h"

class Bakery : public IBakery {
public:
 Bakery(void);
 ~Bakery(void);
 
 // IUnknown functions
 virtual HRESULT __stdcall QueryInterface(const IID& interfaceid, void** objectInterface);
 virtual ULONG __stdcall AddRef();
 virtual ULONG __stdcall Release();

 virtual HRESULT getNumberCookiesBaked(long * number);
 virtual HRESULT bakeCookies(long amount, IUnknown * jar);

private:
 /** Number of cookies this Bakery has baked */
 long cookiesBaked;
 /** Bakery reference count */
 volatile LONG m_referenceCount; 
};

class CFactory : public IClassFactory
{
public:
 // IUnknown functions
 virtual HRESULT __stdcall QueryInterface(const IID& interfaceid, void** objectInterface);
 virtual ULONG __stdcall AddRef();
 virtual ULONG __stdcall Release();

 // IClassFactory functions
 virtual HRESULT __stdcall CreateInstance(IUnknown* outerAggregateIUnknown, const IID& interfaceid, void** objectInterface);
 virtual HRESULT __stdcall LockServer(BOOL shouldLock);

 CFactory() : m_referenceCount((LONG)1) {}
 ~CFactory() {}

private:
 volatile LONG m_referenceCount;
};

Make a C++ file to implement the IBakery.idl interface.

// Bakery.cpp

#include "Bakery.h"
#include "jar.tlh"
#include <stdexcept>
 
static volatile LONG g_serverLockCount = 0;
static CFactory classFactory;

EXTERN_C HRESULT DllGetClassObject(REFCLSID requestedClassID, REFIID requestedInterfaceID, LPVOID * objectInterface) ...

HRESULT Bakery::bakeCookies(long amount, IUnknown * jar) {
 GUID IID_IJar = __uuidof(Jar::IJar);
 Jar::IJar * jarinstance;
 long hr = jar->QueryInterface(IID_IJar, (void**)&jarinstance);
 if (FAILED(hr)) return hr;

 // C++ calling a method on a C# object
 hr = jarinstance->AddCookies(amount);
 if (FAILED(hr)) return hr;
 cookiesBaked += amount;

 return S_OK;
}

... definiton of Bakery and CFactory methods in Bakery.h ...

Create components.map

In Windows, COM servers are registered in the Windows Registry. libcom instead uses GUID to DLL mapping information recorded in a text file, components.map.

Access C# COM server from C++

Create a C# COM server

Put attributes on the interface, interface methods, and class.

// Jar.cs

using System;
using System.Runtime.InteropServices;
namespace Kitchen
{
 [ComVisible(true)]
 [Guid("5e139206-aca8-4530-b2b9-308e16f81bda")]
 [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
 public interface IJar
 {
  [DispId(1)]
  int GetNumberCookies();
  [DispId(2)]
  void AddCookies(int number);
 }

 [ComVisible(true)]
 [Guid("b1de8884-c20c-4743-a1b3-851daad6d188")]
 [ClassInterface(ClassInterfaceType.None)]
 [ProgId("Kitchen.Jar")]
 public class Jar : IJar
 {
  public Jar() {}
  public int GetNumberCookies() { return m_numberCookies; }
  public void AddCookies(int number) { m_numberCookies += number; }
  private int m_numberCookies;
 }
}

Create C++ headers for the C# class

Build and copy the Jar.dll assembly to Windows. Create a TLB from Jar.dll:

Windows$ tlbexp Jar.dll /out:Jar.tlb

Create a one-line file, temp.cpp, with contents:

#import "Jar.tlb"

Create C++ headers from Jar.tlb:

Windows$ cl /LD temp.cpp

Copy the resulting jar.tlh,jar.tli to Linux.

Tweak jar.tlh,jar.tli to make them compile.

Bakery.cpp #includes jar.tlh.

Process jar.tlh to produce Jar_GUIDs.cpp.

Create test program

// TestProgram.cs
using System;
using System.Runtime.InteropServices;
using System.Reflection;
using IBakeryMetadata; // C# stubs to C++ class

public class TestProgram {
 public static void Main(string[] args) {

  // Create C++ object
  Bakery bakery = new Bakery();
  // Create C# object
  Kitchen.Jar jar = new Kitchen.Jar();

  // Pass C# object to C++ object
  bakery.bakeCookies(8, jar);

  System.Console.WriteLine("Number of cookies in jar: " + jar.GetNumberCookies());
  System.Console.WriteLine("Number of cookies baked: " + bakery.getNumberCookiesBaked());
 }
}

Compiling

$ g++ -I.../libcom-headers -c Bakery.cpp
$ g++ -I.../libcom-headers -c IBakery_GUIDs.cpp
$ g++ -lcom IBakery_GUIDs.o Bakery.o -shared -o libbakery.so
$ gmcs -out:TestProgram.exe TestProgram.cs -r:IBakeryMetadata.dll -r:Jar.dll

Explanation of what's happening

In C#, the test program creates a C# Jar and also requests the creation of a Bakery (which it sees as C#). Mono loads libcom (via symlink from ole32.dll) and calls CoCreateInstance. libcom finds the Bakery .so, loads it, creates a Bakery, and gives it to Mono. Mono makes an RCW to present the Bakery C++ COM server to the C# test program as though it were a C# object.

The C# test program then calls a method on Bakery and passes the C# Jar to it as an argument. Mono creates a CCW for Jar so that C++ can access Jar through a COM interface and call methods on Jar as though it were C++.

In C++, a C# Jar method (AddCookies) is called to increment a counter in the Jar, and then the C++ Bakery method returns.

Back in C#, the C# Jar is examined, and the counter is seen to have been incremented.



Code examples are Copyright (C) 2007 SIL International and licensed under the LGPL. Details are in the real files in the source repository.