**tcom server** Use [tcom] as an Active-X server, e.g. create com objects with tcl. [Chin Huang] creates and maintains [tcom]. [HaO] has created this page on 2012-08-31 within a project with a wrapped application and without deep knowledge of COM in general. I personally made a trial and error process and was surprised that, at the end, something worked. I hope this might be useful to someone. '''Feel free to change anything !''' Table of contents: <> ---- [http://www.vex.net/~cthuang/tcom/server.html%|%Original documentation%|%] ---- There are two connection methods to propose a com object to other applications: ***Running object*** The server aplication is already running and creates a com object without and client contact. The com object is registered in the [http://www.thrysoee.dk/InsideCOM+/ch11g.htm%|%running object table (ROT)%|%] of windows. This table is not contained in the registry but in memory. Registry entries are only used for helper issues. TCOM clients contact those objects using [http://www.vex.net/~cthuang/tcom/tcom.n.html%|%getactiveobject%|%]: ***Server object*** The TCL server application is started by the client and may be incorporated as DLL in the same process as the client or might be in another process. The registry is used to inform the client application about the location of the com server. TCOM clients contact those objects using [http://www.vex.net/~cthuang/tcom/tcom.n.html%|%createobject%|%]. Here are three variants: ****Inproc**** The COM object server is loaded as a DLL in the same process as the client application. ****Local**** The COM object server is loaded in another process. It might be an exe or a DLL. ****Remote**** The COM object server is loaded on another machine than the client program. It might be an exe or a DLL. The server seams to be identical. The COM machinery will transport the information. ---- The server may be a TCL script invoked by the tcl dll library or a wrapped exe file. Here is an overview, which method might be with which file type: %||Running object|Inproc Server object DLL|Local or Remote Server object |% &|TCL script|yes|yes|yes|& &|Wrapped exe|yes|no|yes|& **TCL script** The [http://www.vex.net/~cthuang/tcom/server.html%|%original documentation%|%] describes this implementation method. [Chin Huang] explained to [Jeff Godfrey] on [http://groups.google.com/group/comp.lang.tcl/browse_thread/thread/6f6ecd7ca12686b7%|%clt%|%] what really happens in the command [http://www.vex.net/~cthuang/tcom/server.html%|%::tcom::server register%|%]: ======none The tcom COM server implementation starts up a Tcl interpreter for each COM server, so I don't see how it can invoke the code in a wrapped application. The Tcl command ::tcom::server register Banking.tlb creates the Windows registry key HKEY_CLASSES_ROOT\\CLSID\\$clsid\\tcom with two values: "TclDLL" contains the full path to the Tcl interpreter DLL to load. "Script" contains Tcl code which the Tcl interpreter will execute to load and register the Tcl implementation of that COM class. The script is simply "package require Banking" because the Banking package encapsulates the COM class implementation. ====== I personally have no experience with this method, please follow the . **Tools** The following tools are helpful. They are included in many Microsoft development packages like Visual C++ Express. I use `MS-Visual C++ 6.0` and `Microsoft Platform SDK for Windows Server 2003 SP1`. Required command line development tools: * midl.exe: IDL compiler * uuidgen: Generates UID strings Debugging tools * regedit.exe: Show registry, contained in windows * irotview.exe: show the Running Object Table (ROT) * OLE-COM Object viewer: Show COM-View of registry, recompile type libraries to idl * VBA client (Excel, Word, whatever...) to check real type of return values (see chapter VBA-Client) * tcl8.6 command `tcl::unsupported::representation` to check the type of the internal representation of variables Registration tools * regtlib: Register type library, use as administrator **Type library** The first step to a COM server is the creation of a type library. To create a type library, first an interface definition file must be written. An example is below. Its properties in descending logical order are: * File name: com_link_process.idl * Library name: Application * Object class: Step (coclass clause) * Interface name: IStep * Method name: Process * Input parameter: pIn of type `binary string pointer` (BSTR *). * return value: pRet of type `binary string pointer` (BSTR *). ======none import "oaidl.idl"; import "ocidl.idl"; [ object, uuid(0C7E66F0-B50E-4B03-B784-2C89A9069B65), dual, helpstring("COM Link Step Interface"), pointer_default(unique) ] interface IStep: IDispatch { [id(1), helpstring("COM Link Process")] HRESULT Process( [in] BSTR *pIn, [out, retval] BSTR *pRet); }; [ uuid(16A8EC94-8E85-4D72-9425-B0C64E3BF6A8), version(1.0), helpstring("COM Link Process 1.0 Type Library") ] library Application { importlib("stdole32.tlb"); [ uuid(A7CB39F0-4996-4A43-AD5A-1C718D41CB98), helpstring("COM Link Step Class") ] coclass Step { [default] interface IStep; }; }; ====== ***uuid*** An '''uuid''' is required for the identification of all items. Those may be generated by: ======none C:\test>uuidgen 09e8f1b9-5d1a-409e-9a07-bba3dfbbdf5c ====== I experienced that it may be helpful to put them in uppercase. ***generate type library file*** To generate a type library file (.tlb) from an interface definition file, one may use `midl`: ======none C:\test>midl com_link_process.idl ====== The generated file '''com_link_processs.tlb''' must be copied to the server project files. In this example, it is copied in the same folder as the wrapped server executable. Thus, the file name is: ====== set Filename [file join [file dirname [info nameofexecutable]] com_link_process.idl] ====== ***Register type library*** Within this context, registering the type library seams not to be of any use. It could be done using `regtlib `. It might be helpful for any programming frameworks. The registry subtree is `HKEY_LOCAL_MACHINE\SOFTWARE\Classes\TypeLib\{}`. ***Load type library*** The first step of the com server script is to import the type library: ====== if {[catch { package require tcom ::tcom::import $Filename } errMsg]} { tk_messageBox -message "Type library file '$Filename' missing.\n$Err" exit } ====== This creates the command '''::Application::Step'''. If the type library file is included in a wrapped application, it must first be copied out of it: ====== set Filename [file join $::env(Tmp) com_link_process.idl] file copy -force -- [file join [file dirname [info script]] com_link_process.idl] $Filename ====== **::tcom::object command** All server objects are created using this command family: ====== ::tcom::object registerfactory factorycmd ?deletecmd? ::tcom::object create ?-registeractive? methodcmd ?deletecmd? ====== All parameters with cmd are evaluated. They are aranged to well support IncrTCL Objects. In contrast, the examples use plain TCL: +++ factorycmd is invoked on a client connect. The returned result of each invocation is registered as `methodcmd`. methodcmd method ?arg?... is invoked when a method or property is invoked by the client. deletecmd methodcmd is invoked when the client frees the object with the registered `methodcmd` as parameter. +++ **Running object** The exposed object of the server is created by: ====== set objectHandle [::tcom::object create -registeractive ::Application::Step ObjectCallback ObjectDeleteCallback] ====== The procedure '''ObjectCallback''' is invoked, if a client invokes the method '''Process''' of the step. The procedure '''ObjectDeleteCallback''' is invoked with the parameter '''ObjectCallback''' when the client deletes the object. The MS-Visual tools contain a program '''ROT Viewer'''. The object may now be seen by the ROT-Viewer by its class uuid: "A7CB39F0-4996-4A43-AD5A-1C718D41CB98". ***Object handling procedure*** Each call to the object invokes the object handling procedure '''ObjectCallback'''. ====== proc ObjectCallback {method args} { switch -exact -- $method { Process { return [::tcom::variant bstr "[lindex $args 0] ok"] } default { return -code error "Unknown method '$method'" } } } ====== Some hints about the procedure: * Be shure, that the return value has the right internal representation. See the [tcom] page for hints. I use for conversion: ** string: `[::tcom::variant bstr $var]` (doc see separate chapter, its use in the example is academic and thus for demonstration). ** list: `llength $var` * Get and Set calls get the following prefixes to the method: `_get_` and `_set_`. * Script errors are sent to the server (which is handy). I personally catch the whole procedure at least to log error in the server. * Often, a Quit method is foreseen to quit the application. The corresponding program exit should be delayed to correctly return the com request (`after idle {exit 0}`). ****Argument handles**** When passing VBA variables as arguments, there is a tcom handle given as argument: %|VBA Command|arg0 parameter value|% %|Process("a")|a|% &|va=5,Process(va)|::tcom::handle0x034EC350|& &|Process(va+"")|a|& The passed handle has the following interface: %|Property|Value|% &|name|IVariable|& &|properties|{0 {in out} BSTR Value}|& &|iid|abe2cf86-7fca-42e8-b84c-7fb73363c190|& &|methods|{0 BSTR Value {}} {0 VOID Value {{in BSTR propertyValue}}} {2 I4 SetValue {{in BSTR Value}}}|& Thus, the following code might be used for a pre-treatment of the variables: ====== proc ObjectCallback {method args} { set lParams {} foreach ParCur $args { if { [string match ::tcom::handle0x* $ParCur] && 0 != [llength [info commands $ParCur]] } { set ParCur [$ParCur Value] } lappend lParams $ParCur } switch -exact -- $method { Process { return [::tcom::variant bstr "[lindex $lParams 0] ok"] } default { return -code error "Unknown method '$method'" } } } ====== ***Object delete procedure*** The object delete procedure is called when the client deletes the object. To have a constant registered object, one may reregister an object: ====== proc ObjectDeleteCallback {args} { ::tcom::object create -registeractive ::Application::Step ObjectCallback ObjectDeleteCallback } ====== ***Invoke the object by its class id*** Another interpreter may now access the object by its class uuid and invoke the process method: ====== % package require tcom % set h [::tcom::ref getactiveobject -clsid A7CB39F0-4996-4A43-AD5A-1C718D41CB98] ::tcom::handle0x02715AA9 % $h Process A A OK ====== ***Register Program ID*** To use the program ID '''Link3.Application''' instead the class ID, the following registry keys are necessary to set: ====== package require registry registry set {HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Link3.Application} "" "EasySoft.Link" registry set {HKEY_LOCAL_MACHINE\SOFTWARE\Classes\Link3.Application\CLSID} "" "{A7CB39F0-4996-4A43-AD5A-1C718D41CB98}" ====== Explanations: [http://msdn.microsoft.com/en-us/library/windows/desktop/dd542719%28v=vs.85%29.aspx] The first key must be of the format [http://msdn.microsoft.com/en-us/library/ee487753.aspx%|%vendor.component%|%]. An included space resulted in the windows api function '''CLSIDFromProgID''' to return an error. This must be done as administrator or may be done by the installation routine. ***Invoke the object by its program id*** Another interpreter may now access the object by its program id and invoke the Process method: ====== % package require tcom % set h [::tcom::ref getactiveobject Link3.Application] ::tcom::handle0x02715AA8 % $h Process A A OK ====== **(Wrapped) Local Server** The server is now started using registry keys. The upper example is continued and the local server capacity is added to the same script. ***Registry registration*** They should be installed by a command line switch `/regserver` and uninstalled by `/unregserver`. It is a convenient way to start the exe in the installation routine to do the registration. My code: ====== if { "/regserver" in $::argv || "/unregserver" in $::argv } { if {[catch { package require registry set Key {HKEY_LOCAL_MACHINE\SOFTWARE\Classes} if {"/regserver" in $::argv} { # map Link3.Application to class uuid registry set $Key\\Link3.Application "" Link3 registry set ${Key}\\Link3.Application\\CLSID ""\ \{A7CB39F0-4996-4A43-AD5A-1C718D41CB98\} # execute exe if class required append Key \\CLSID\\\{A7CB39F0-4996-4A43-AD5A-1C718D41CB98\} registry set $Key "" Link3 registry set $Key\\LocalServer32 ""\ [file nativename [file normalize [info nameofexecutable]]] registry set $Key\\ProgID "" Link3.Application } else { registry delete $Key\\Link3.Application registry delete $Key\\CLSID\\\{A7CB39F0-4996-4A43-AD5A-1C718D41CB98\} } } Err]} { tk_messageBox -message "Error registering com server.\nPropably not administrator" exit 1 } exit 0 } ====== Key explanation: [http://msdn.microsoft.com/en-us/library/windows/desktop/ms691424%28v=vs.85%29.aspx] ***Register COM client connection callback*** The following command registers a callback when a client connects the object: ====== set objectHandle [::tcom::object registerfactory ::Application::Step ObjectFactoryCallback ObjectDeleteCallback2] ====== The called procedure must return the method callback command: ====== proc ObjectFactoryCallback {} { return ObjectCallback } ====== The Object delete callback is different to the registered object, as it is not automatically recreated. Here we do nothing, but any cleanup code may be placed here. ====== proc ObjectDeleteCallback2 {} { } ====== We are ready to call the com server from another instance: ====== % set h [::tcom::ref createobject Link3.Application] ::tcom::handle0x02930090 % $h Process a a OK ====== The server will be started with the command line switch '''-Embedding''' and should eventually not show any gui. **VBA Client** Now, Visual Basic for Applications (VBA) is used as client (not tcom). Here are some observations: * Variables in VBA tend to be of type `variant`. * The influence of variable types in IDL is not obvious to me... * To check parameter type in VBA, use `VarType()` and find the return value in the internet. Do not use `VarName()` as it does not show array types. * If the TCL object callback function returns with a value which have the internal representation of a list, VarType() is [http://www.chennaiiq.com/developers/reference/visual_basic/functions/vartype.asp%|%8200%|%]. The value may be acced within VBA using `varname(0)`, `varname(1)`... * Be shure, that the function parameters have the right type. I observed values like `::tcom::handle0x045BAA00` for a `BSTR` parameter when passing a variable with string contents. A dummy operation helped: `"" + varname`. * To be able to pass a VBA variable, use IDL type `VARIANT` and return by `return [::tcom::variant bstr $data]`. A VBA script to use the upper com object: ====== set oLink3 = CreateObject("Link3.Application") Variable = oLink3.Process("a") ====== ---- **::tcom::variant command** This (AFAIK undocumented) command seams to be helpful to me to construct a variant of a given type: ====== ::tcom::variant type ?data? ====== `type` is one of: empty, null, i2, i4, r4, r8, cy, date, bstr, dispatch, error, bool, variant, unknown, decimal, record, i1, ui1, ui2, ui4, i8, ui8, int, uint Example: ====== % set v [::tcom::variant bstr abc] abc set b [::tcom::variant bool true] -1 ====== Internally, a tcl variable with the custom type `VARIANT` is created. The examples are wrong in this point, that here the variable value is shown to the console and thus directly reconverted to a string. Helpful documentation: * [http://tester.poleyland.com/tester/publications/Black%20Book.pdf%|%Black Book, Page 4%|%] * [http://msdn.microsoft.com/en-us/library/cc237562%28v=prot.13%29%|%2.2.49.3 Automation-Compatible Types%|%] ---- **Arrays** I managed to return array results to VBA. I did not manage to pass VBA arrays as parameters (type mismatch error). ***IDL Method declaration*** ======none [id(14), helpstring("Get list data as variant")] HRESULT ListGetVariant( [out, retval] SAFEARRAY(VARIANT) *pavValue ); ====== ***TCL Object callback function fragment*** The function constructs a list of variant items. ====== switch -exact -- $Method { ListGetVariant { set lRet {} foreach Item $lData { lappend lRet [::tcom::variant bstr $Item] } return $lRet } } ====== ***VBA client code*** ====== set oLink3 = CreateObject("Link3.Application") ArrayValue = oLink3.ListGetVariant() MsgBox VarType(ArrayValue) ScalarValue = ArrayValue(0) ====== The Type 8204 `Array of Variants` is printed by the message box. The type would be `8200` (Array of Strings), when `VARIANT` is replaced by `BSTR` in IDL (if I remember well). Then, you get an error in the last VBA script row. ---- **Open Issues** The following issues are open to me: * How may callbacks/events be used ? ---- **My Wish list** * 64 Bit Version * No dependency on the internal representation of TCL variables... <> COM | Example | Package | Windows | Interprocess Communication