COM Object Implementation in Tcl

Introduction

This article shows by example how to implement COM objects in Tcl with the tcom extension. It shows how an object can be implemented by an [incr Tcl] class or in just plain Tcl.

The class diagram shows the structure of server objects which implement two COM interfaces. The IAccount interface defines a Balance property, and Deposit and Withdraw methods which modify the balance. The Account class implements the IAccount interface by delegating its operations to the AccountImpl class, which is written in [incr Tcl] and actually implements the operations. The IBank interface defines a method to create an account. Following the same pattern, the Bank class implements the IBank interface by delegating to the BankImpl class, which provides the actual implementation.

Write MIDL Specification

The file Banking.idl contains the MIDL specification for the COM interfaces and classes. The interfaces can be declared dual because tcom can implement objects whose operations are invoked through the IDispatch interface or the virtual function table.

import "oaidl.idl";
import "ocidl.idl";

        [
                object,
                uuid(0A0059C4-E0B0-11D2-942A-00C04F7040AB),
                dual,
                helpstring("IAccount Interface"),
                pointer_default(unique)
        ]
        interface IAccount: IDispatch
        {
                [id(1), propget, helpstring("property Balance")]
                HRESULT Balance([out, retval] long *pValue);

                [id(2), helpstring("method Deposit")]
                HRESULT Deposit([in] long amount);

                [id(3), helpstring("method Withdraw")]
                HRESULT Withdraw([in] long amount);
        };

        [
                object,
                uuid(0A0059C4-E0B0-11D2-942A-00C04F7040AC),
                dual,
                helpstring("IBank Interface"),
                pointer_default(unique)
        ]
        interface IBank: IDispatch
        {
                [id(1), helpstring("method CreateAccount")]
                HRESULT CreateAccount([out, retval] IAccount **pAccount);
        };

[
        uuid(0A0059B8-E0B0-11D2-942A-00C04F7040AB),
        version(1.0),
        helpstring("Banking 1.0 Type Library")
]
library Banking
{
        importlib("stdole32.tlb");

        [
                uuid(0A0059C5-E0B0-11D2-942A-00C04F7040AB),
                helpstring("Account Class")
        ]
        coclass Account
        {
                [default] interface IAccount;
        };

        [
                uuid(0A0059C5-E0B0-11D2-942A-00C04F7040AC),
                helpstring("Bank Class")
        ]
        coclass Bank
        {
                [default] interface IBank;
        };
};

Create Type Library

Run this command to generate a type library file Banking.tlb from the MIDL specification.

midl Banking.idl

Create Tcl Package

The tcom server implementation depends on the Tcl package mechanism to provide the code that implements specific COM interfaces. In this example, we'll create a package named Banking, which provides code that implements the IBank and IAccount interfaces.

Create a directory for the package by making a subdirectory named Banking under one of the directories in the auto_path variable. Create a pkgIndex.tcl file in the package directory.

package ifneeded Banking 1.0 [list source [file join $dir server.itcl]]

Copy the Banking.tlb type library file into the package directory.

Create the following server.itcl file in the package directory. This file defines [incr Tcl] classes that implement the IBank and IAccount interfaces.

package provide Banking 1.0

package require Itcl
namespace import ::itcl::*

package require tcom
::tcom::import [file join [file dirname [info script]] Banking.tlb]

class AccountImpl {
    private variable balance 0

    public method _get_Balance {} {
        return $balance
    }

    public method Deposit {amount} {
        set balance [expr $balance + $amount]
    }

    public method Withdraw {amount} {
        set balance [expr $balance - $amount]
    }
}

class BankImpl {
    public method CreateAccount {} {
        set accountImpl [AccountImpl #auto]
        return [::tcom::object create ::Banking::Account \
            [code $accountImpl] {delete object}]                                ;# 1
    }
}

::tcom::object registerfactory ::Banking::Bank {BankImpl #auto} {delete object} ;# 2

On line 1, the ::tcom::object create command creates a COM object that implements the IAccount interface by delegating its operations to an [incr Tcl] object specified by an [incr Tcl] object handle. Interface methods are mapped to a method with the same name. Interface properties are mapped to methods named by prepending _get_ and _set_ to the property name. When the last reference to the COM object is released, tcom invokes the delete object command with the [incr Tcl] object handle as an additional argument to clean up the [incr Tcl] object.

Line 2 creates a factory for creating instances of the Bank class and registers the factory with COM. To create a COM object, the factory invokes a command which returns a handle to an [incr Tcl] object that implements the operations. In this example, the factory invokes the BankImpl #auto command which creates a BankImpl [incr Tcl] object and returns a handle to that object. To clean up when the COM object is destroyed, tcom invokes the delete object command with the [incr Tcl] object handle as an additional argument.

Register Server

Run these Tcl commands to create entries in the Windows registry required by COM and the tcom server implementation.

package require tcom
::tcom::server register Banking.tlb

Implement Client

The client.tcl script implements a simple client. It gets a reference to an object that implements the bank interface, creates an account, and performs some operations on the account.

package require tcom

set bank [::tcom::ref createobject "Banking.Bank"]
set account [$bank CreateAccount]
puts [$account Balance]
$account Deposit 20
puts [$account Balance]
$account Withdraw 10
puts [$account Balance]

Implement Objects In Plain Tcl

You can implement objects in plain Tcl. The servant command passed to the ::tcom::object create command can be the name of any object-style command. Similarly, the factory command passed to the ::tcom::object registerfactory command can return the name of any object-style command. The following Tcl script defines the procedures accountImpl and bankImpl, which have parameters in the style of a method name followed by any arguments.

package provide Banking 1.0

package require tcom
::tcom::import [file join [file dirname [info script]] Banking.tlb]

proc accountImpl {method args} {
    global balance

    switch -- $method {
        _get_Balance {
            return $balance
        }

        Deposit {
            set amount [lindex $args 0]
            set balance [expr $balance + $amount]
        }

        Withdraw {
            set amount [lindex $args 0]
            set balance [expr $balance - $amount]
        }
        
        default {
            error "unknown method $method $args"
        }
    }
}

proc bankImpl {method args} {
    global balance

    switch -- $method {
        CreateAccount {
            set balance 0
            return [::tcom::object create ::Banking::Account accountImpl]
        }
        
        default {
            error "unknown method $method $args"
        }
    }
}

::tcom::object registerfactory ::Banking::Bank {list bankImpl}