Marshaling von benutzerdefinierten Datentypen

Veröffentlicht: 14. Jun 2000 | Aktualisiert: 16. Jun 2004

Von Michael Willers

Der Universal Marshaler, den es seit Windows 98 und Windows NT 4 / SP4 gibt, kann nun auch mit benutzerdefinierten Datentypen umgehen. Er erspart somit das Erstellen und Registrieren der Proxy-/Stub-DLL.

Auf dieser Seite

 An die Tasten!
 Implementierung der Schnittstelle
 Und nun zum Client
 GREP hilft weiter

Diesen Artikel können Sie hier lesen dank freundlicher Unterstützung der Zeitschrift:

Bild02

Der Einsatz benutzerdefinierter Datentypen ist unter Visual Basic sehr einfach. Daher wollen wir im Folgenden Schritt für Schritt den Einsatz unter Visual C++ beschreiben, damit Sie nicht den üblichen Zyklus "Debuggen, diskutieren und fluchen" durchlaufen müssen. Die Visual Basic-Freaks möchten wir auf die MSDN-Bibliothek verweisen. Sie finden unter folgenden Begriffen Einträge über dieses Thema:

  • "HOWTO: Remote User-Defined Types (Q185700)"

  • "PRB: Passing a UDT To Or From an ActiveX EXE May Fail on NT (Q187922)"

An die Tasten!

Werfen Sie Visual C++ an und lassen Sie sich mit dem dem ATL-COM-AppWizard das Grundgerüst für einen COM-Server (Servertyp DLL) erstellen. Fügen Sie anschließend mit dem ATL-Object-Wizard ein neues COM-Objekt (Simple Object) hinzu. Sie können dabei die Standardeinstellungen übernehmen.
Im nächsten Schritt werden die benutzerdefinierten Datentypen von Hand in der IDL-Datei eingetragen. Dazu müssen Sie die notwendigen GUIDs mit dem Programm Guidgen.exe erstellen, das Sie bei den Visual Studio-Dienstprogrammen finden, und via Zwischenablage in Ihre Quelltextdatei übernehmen.:

Als Beispiel haben wir einen Datentyp TPerson erstellt, der seinerseits von einer Aufzählung TDepartment abhängig ist (Listing L1). Dieser Datentyp wird nun in einem Interface ICMyUDT benutzt, dessen Methoden die verschiedenen Arten der Parameterübergabe (By Value bzw. By Reference und als Rückgabewert) zeigen.

Einstellungen beim Erstellen der GUIDs.

Bild01

Listing L1: Die IDL-Datei des Beispiels

// UDTTest.idl : IDL source for UDTTest.dll 
// 
// This file will be processed by the MIDL tool to 
// produce the type library (UDTTest.tlb) and marshaling code. 
import "oaidl.idl"; 
import "ocidl.idl"; 
[ 
    uuid(FC126BC1-1EAC-11D3-996A-4C1671000000), 
    version(1.0), 
    helpstring("UDTTest 1.0 Type Library") 
] 
library UDTTESTLib 
{ 
    importlib("stdole32.tlb"); 
    importlib("stdole2.tlb"); 
    // Abteilungen als enum 
    [ 
        uuid(1EC3BB00-1EAE-11d3-996A-4C1671000000), 
        version(1.0) 
    ] 
    typedef enum TDepartment 
    { 
        Development = 0x000000FF, 
        Procurement = 0x0000FF00, 
        Education = 0x00FF0000, 
        Unknown    = 0xFF000000 
    } TDepartment; 
    // Benutzerdefinierter Datentyp 
    [ 
        uuid(62D33614-1860-11d3-9954-10C0D6000000), 
        version(1.0) 
    ] 
    typedef struct TPerson 
    { 
        BSTR bstrFirstname; 
        BSTR bstrLastname; 
        long lAge; 
        TDepartment Dep; 
    } TPerson; 
    // Interface 
    [ 
        object, 
        uuid(FC126BCD-1EAC-11D3-996A-4C1671000000), 
        dual, 
        helpstring("ICMyUDT Interface"), 
        pointer_default(unique) 
    ] 
    interface ICMyUDT : IDispatch 
    { 
        [id(1), helpstring("method PassUdtByRef")] HRESULT  
            PassUdtByRef([ref, in, out] TPerson* pPerson); 
        [id(2), helpstring("method ReturnUdt")] HRESULT ReturnUdt( 
            [out, retval] TPerson* pPerson); 
        [id(3), helpstring("method PassUdtByVal")] HRESULT  
            PassUdtByVal([in] VARIANT varPerson); 
    }; 
    // coclass 
    [ 
        uuid(FC126BCE-1EAC-11D3-996A-4C1671000000), 
        helpstring("CMyUDT Class") 
    ] 
    coclass CMyUDT 
    { 
        [default] interface ICMyUDT; 
    }; 
};

Sie können nun mit [CTRL] + [F7] die Typelib kompilieren lassen und anschließend die Schnittstelle implementieren. Die Warnung MIDL2039 können Sie getrost ignorieren, denn der MIDL-Compiler von Visual C++ 6.0 kennt den neuen Universal Marshaler noch nicht.

 

Implementierung der Schnittstelle

Die beiden ersten Methoden der Schnittstelle sind nicht sonderlich aufregend, da passiert nichts Neues. Interessant ist allerdings die Methode PassUdtByVal. Hier wird eine Datenstruktur in einem Variant übergeben (Listing L2).
Bislang funktionierte das nur mithilfe eines SafeArrays: Eine Struktur wird in Feldinhalte zerlegt und später aus diesen Feldinhalten wieder zusammengesetzt. Dieser Aufwand ist jetzt nicht mehr notwendig. Man kann eine Struktur nun direkt an einen Variant übergeben und wieder auslesen. Dazu führt der neue Universal Marshaler den Variant-Typ VT_RECORD sowie die Variable pvRecord ein.

L2 Auslesen einer Struktur ohne SafeArray

STDMETHODIMP CCMyUDT::PassUdtByVal(VARIANT varPerson) 
{ 
    // Achtung: VT-Record ist neu in NT4 SP4 und Win98 
if (varPerson.vt != VT_RECORD) return E_INVALIDARG; 
TPerson* pPerson = static_cast<TPerson*>(varPerson.pvRecord); 
    // zuweisen 
    m_Person.bstrFirstname = ::SysAllocStringLen( 
        (*pPerson).bstrFirstname, 
        ::SysStringLen((*pPerson).bstrFirstname)); 
    m_Person.bstrLastname = ::SysAllocStringLen( 
        (*pPerson).bstrLastname, 
        ::SysStringLen((*pPerson).bstrLastname)); 
    m_Person.lAge = (*pPerson).lAge; 
    m_Person.Dep = (*pPerson).Dep; 
    return S_OK; 
}

Dabei sollten Sie darauf achten, dass die Zuweisung der Zeichenketten mit der Funktion SysAllocStringLen erfolgt, damit ein komplett neuer Wert zugewiesen wird.
Damit ist die Schnittstelle implementiert und Sie können mit [F7] die COM-DLL erstellen.

 

Und nun zum Client

Sofern Sie Ihre Clients ausschließlich mit Visual Basic implementieren, können Sie jetzt weiterblättern und sich anderen interessanten Artikeln widmen. Visual Basic nimmt Ihnen die restliche Arbeit ab: Sie können Strukturen direkt an einen Variant übergeben:

Private Sub Command3_Click() 
    With udtPerson 
        .bstrFirstname = "Wilhelm" 
        .bstrLastname = "Willemsen" 
        .lAge = 82 
        .Dep = Procurement 
    End With 
    objPerson.PassUdtByVal udtPerson 
End Sub

Unter Visual C++ ist das leider nicht ganz so einfach. Da ist ein wenig Handarbeit angesagt und wir zeigen anhand eines MFC-Clients, wie man dabei vorgeht. Die gewonnenen Erkenntnisse sind allerdings sehr allgemein gehalten und können leicht in andere Programme wie zum Beispiel Konsolenanwendungen übernommen werden.
Erstellen Sie zunächst mit dem AppWizard eine dialogbasierte MFC-Anwendung. Nutzen Sie anschließend das #import-Statement, um einen Smartpointer sowie Wrapper-Klassen für den Zugriff auf unser COM-Objekt zu erzeugen. Fügen Sie dann der Dialogklasse einen Destruktor hinzu sowie Member-Variablen für den Smartpointer und die Struktur:

 // UDTTestClientDlg.h : header file 
    . 
    . 
    . 
#import "..\\ATL-Server\\Debug\\UDTTest.dll" no_namespace 
class CUDTTestClientDlg : public CDialog 
{ 
// Construction 
public: 
    CUDTTestClientDlg(CWnd* pParent = NULL); 
~CUDTTestClientDlg(); 
    TPerson m_Person; 
    ICMyUDTPtr m_UDTPtr; 
     // Dialog Data 
    . 
    . 
    .

Die Anmeldung und Abmeldung beim COM-Laufzeitsystem und das Anlegen des Smartpointers können dann im Konstruktor bzw. Destruktor erfolgen:

//Initialisieren 
CoInitialize(NULL); 
m_UDTPtr.CreateInstance(__uuidof(CMyUDT)); 
// entsorgen 
m_UDTPrt = 0; 
CoUninitialize();

Fügen Sie dem Dialogfenster für jede Funktion unseres COM-Objekts eine Schaltfläche hinzu und erstellen Sie die dazugehörigen Nachrichtenbearbeiter (Handler). Jetzt müssen Sie nur noch die Handler implementieren, und fertig ist der MFC-Client...
...und genau da schnappt die Falle zu!
Die Handler für den Aufruf der Methoden PassUdtByRef und ReturnUdt des COM-Objekts sind trivial. Aber wie schafft man es, die Struktur in den Variant zu bringen? Eine erste Idee mag vielleicht so aussehen:

. 
. 
. 
TPerson Person = { 
    ::SysAllocString(L"Wilhelm"), 
    ::SysAllocString(L"Willemsen"), 
    82L, 
    Procurement 
}; 
VARIANT varPerson; 
::VariantInit(&varPerson); 
varPerson.vt = VT_RECORD; 
VarPerson.pvRecord = &Person; 
// Funktion aufrufen 
m_UDTPtr -> PassUdtByVal(varPerson); 
. 
. 
.

Diese Idee geht jedoch leider gründlich schief. Die OLEAUT32.DLL meldet eine "Unhandled Exception", noch bevor die Anwendung überhaupt in der COM-DLL "ankommt" (Breakpoints sind doch was feines!).

 

GREP hilft weiter

Das GREP-Tool eines Kollegen, das die API-Funktion GetRecordInfoFromGuids zutage förderte, lieferte des Rätsels Lösung:
Wenn man eine Struktur an einen Variant übergeben möchte, reicht es nicht aus, diese Struktur einfach nur zuzuweisen. Vielmehr muss man dem System über die COM-Schnittstelle IRecordInfo mitteilen, wie diese Struktur aufgebaut ist und wieviel Speicher sie benötigt.
Deshalb gibt es neben dem Typ VT_RECORD und der Variablen pvRecord zusätzlich die Variable pRecInfo. Sie enthält einen Zeiger auf diese Schnittstelle. Und eben dieser Zeiger zeigte ins Nirvana und verursachte somit den Absturz. Besetzt man ihn mit NULL, gibt COM wie erwartet den Fehler "Invalid Parameter" zurück.
Nun stellt sich die Frage, wie man einen gültigen pRecInfo-Zeiger erzeugt. Und Sie ahnen es schon: Dafür ist die Funktion GetRecordInfoFromGuids zuständig.
Sie erzeugt anhand der Informationen in der TypeLib einen gültigen Zeiger auf die Schnittstelle IRecordInfo und man kann dann mit deren Methoden CreateRecord und CopyRecord eine Struktur erzeugen und diese mit Werten besetzen.
Dazu muss man GetRecordInfoFromGuids mit den GUIDs der Struktur und der TypeLib füttern. Diese GUIDs müssen per Zwischenablage aus der IDL-Datei unseres COM-Objekts übernommen werden, weil der MIDL-Compiler die GUIDs nicht mit in die Header-Datei schreibt, die beim Übersetzen der IDL-Datei erzeugt wird. Das fachgerechte Zuweisen der Struktur an eine Variant können Sie in Listing L3 sehen.

L3 Zuweisen einer Struktur an eine Variant

. 
. 
. 
TPerson Person = { 
        ::SysAllocString(L"Wilhelm"),         
        ::SysAllocString(L"Willemsen"),82L,Procurement 
}; 
// GUID von Hand einfügen, da das #import-Statement die nicht mit  
// generiert. Einfach aus IDL-Datei des ATL-Servers kopieren. 
struct __declspec(uuid("62D33614-1860-11d3-9954-10C0D6000000"))  
    TPerson {}; 
struct __declspec(uuid("FC126BC1-1EAC-11D3-996A-4C1671000000"))  
    TTypeLib {}; 
// Informationen für den Variant-Parameter pRecInfo ermitteln.  
// Ist dieser Parameter NULL, gibt es eine Fehlermeldung. Sofern  
// er unbesetzt ist, kann das Programm auch abstürzen. 
IRecordInfo* pRecInfo = NULL;         
HRESULT hRes= ::GetRecordInfoFromGuids( 
   __uuidof(TTypeLib), 1,0,NULL,__uuidof(TPerson),&pRecInfo 
); 
if (FAILED (hRes)) return; 
if (!pRecInfo) return; 
TPerson* pPerson = static_cast<TPerson*>(pRecInfo->RecordCreate());         
pRecInfo->RecordCopy(&Person,pPerson); 
// Den Variant füllen 
VARIANT varPerson; 
::VariantInit(&varPerson); 
varPerson.vt = VT_RECORD; 
varPerson.pRecInfo = pRecInfo; 
varPerson.pvRecord = pPerson; 
// Funktion aufrufen 
m_UDTPtr -> PassUdtByVal(varPerson); 
// Strings wieder freigeben... 
if (0 != ::SysStringLen(Person.bstrFirstname)) 
{ 
    ::SysFreeString(Person.bstrFirstname); 
} 
if (0 != ::SysStringLen(Person.bstrLastname)) 
{ 
    ::SysFreeString(Person.bstrLastname); 
} 
// ...und Variant aufräumen 
::VariantClear(&varPerson); 
. 
. 
.

Nun noch eine kleine Gemeinheit am Rande: Die Zeichenketten der Struktur müssen an Ende mit SysFreeString manuell entsorgt werden. Sonst sind Speicherlecks die Folge.