Author: cgoakley.org
OLE Automation para programadores C ++
Introdução
Eu postei este artigo originalmente em 1997. OLE Automation foi para mim fascinante por causa de sua complexidade e documentação impenetrável, mas quando, entre trabalhos, tive a oportunidade de tentar e cracker de uma vez por todas eu fiz a descoberta bem-vinda de que, embora muito mais complicado do que era necessário, os princípios eram relativamente simples. A tecnologia inclui realmente algumas coisas genuinamente inteligentes, particularmente em relação à comunicação entre processos.
É agora 2012. Desde então, o compilador Microsoft C ++ percorreu um longo caminho, e o .NET aconteceu. Que o .NET deve tanto à COM, em grande parte, endossar os princípios sobre os quais a COM se baseia.
COM, o "Component Object Model" é um padrão de linguagem cruzada desenvolvido pela Microsoft para especificar funções e classes fornecidas por bibliotecas ou executáveis que podem incluir uma descrição binária de conteúdos conhecida como "Biblioteca de tipos". Métodos de objetos COM podem ser executados através dos limites do processo ou da máquina através de um mecanismo denominado "Automação OLE" - embora os termos COM e "OLE Automation" sejam freqüentemente usados indistintamente. Este mecanismo remoting é fornecido de forma transparente e muitas vezes invisivelmente pela Microsoft. A principal razão para se interessar hoje em dia é que é um modelo de objeto COM que está subjacente ao Visual Basic for Applications, e esta ainda é a linguagem de programação no uso mais difundido.
As classes COM bem desenhadas devem ser muito fáceis de usar: é a forma como os não programadores podem fazer programação orientada a objetos.
Criar as classes em C ++, porém, não é tão fácil. A Microsoft criou uma variedade de ferramentas para auxiliar o processo, mas, embora "#import" seja uma benção, a maioria dos outros eu pessoalmente acho inútil: uma característica particularmente irritante é a necessidade de herdar uma implementação de uma classe a partir de uma interface. Isso significa que, para obter um exemplo de financiamento, você não pode herdar ConvertibleBond de Bond - o que você normalmente deseja fazer - porque cada um deve herdar da sua classe de interface correspondente. Existem maneiras de fazer isso, mas são complicadas. Para mim, a hierarquia de classe deve ser a mais apropriada para a tarefa na mão e não deve ser ditada pelo ambiente de programação.
O objeto COM
Visual basic / COM é muito parecido com uma versão reduzida do C ++, mas com uma camada que inclui a verificação de limites automáticos e a contagem de referências para impedir que os scripts do VB falhem no programa. Possui a facilidade adicional de poder executar métodos como chamadas de procedimento remoto. Ao contrário do C ++, isso é incorporado e não requer nenhuma maquinaria extra.
A declaração C ++
ObjectType *T, *TArray[6];
corresponde a
Dim T como ObjectType, TArray (0 a 5) como ObjectType
no VB. "ObjectType" é uma classe definida pelo usuário, que no caso VB é um objeto COM. Internamente, cada objeto é um ponteiro para um objeto C ++ derivado de IDispatch, uma classe abstrata que contém dois serviços principais: (i) coleta de lixo e (ii) dinâmica (ou "late") obrigatório. Examinemos estes por sua vez:
(i) Coleta de lixo
Considere o seguinte pedaço de C++:
ObjectType *T = new ObjectType, *U;
U = T;
T->DoSomething();
delete T;
U->DoSomething();
Claro que isso provavelmente irá bloquear o programa e, claro, você nunca fez isso ou o equivalente, porque você percebe o perigo. Boa. Eu também não. Bem, quase nunca ... A VB tem muito menos confiança em você. A atitude da VB é que você não deve travar o programa em nenhuma circunstância, e assim não permite que você exclua objetos. Em vez disso, o ambiente controla todos os objetos que estão sendo usados no programa e, quando um objeto não está mais em uso, ele é excluído automaticamente. O paradigma que tem exclusão de objetos que não estão em uso controlados pelo meio ambiente em vez do programador é conhecido como Garbage Collection. Em um ambiente coletado por lixo, o código é mais curto, mas, o mais importante, é impossível acessar um objeto excluído ou inválido. O VB equivalente
Dim T As New ObjectType, U As ObjectType
Set U = T
T.DoSomething
Set T = Nothing
U.DoSomething
para o acima não dará um erro porque a tentativa de excluir o objeto por Set T = Nothing remove uma referência, mas na verdade não a exclui. O objeto não será excluído até a variável U sai do escopo. A coleta de lixo é uma característica de todos os objetos COM e é implementada pela derivação de todas as classes COM da classe abstrata IUnknown:
struct IUnknown
{
virtual HRESULT QueryInterface(const GUID * const riid, void **ppvObject) = 0;
virtual unsigned long AddRef() = 0;
virtual unsigned long Release() = 0;
};
Os dois últimos métodos são para coleta de lixo. O interesse em um objeto é expresso criando-o ou ligando AddRef; nenhum interesse adicional é expresso ao chamar Release. O objeto é responsável por excluir-se quando Release é chamado mais uma vez do que havia anterior chamado AddRef. Isso é comumente implementado por ter uma referência de referência longa não assinada m_refs que é configurado para um quando o objeto é construído, com AddRef e Release implementado da seguinte forma:
unsigned long DerivedFromIUnknown::AddRef() {return m_refs++;}
unsigned long DerivedFromIUnknown::Release()
{
if (--m_refs > 0) return m_refs;
delete isso; // destruidor deve ser declarado como virtual para que isso funcione
return 0; // Observe que não devemos fazer referência a m_refs após o "delete isso"
}
(os valores de retorno são apenas para fins de depuração). Este esquema é muito o tipo mais simples de coleta de lixo, mas tem uma advertência importante, e que contribuiu para o desaparecimento da COM. Se objeto A tem objeto B como uma propriedade, com o objeto B simultaneamente possuindo o objeto A como uma propriedade, uma situação conhecida como referência cíclica, essas referências mútuas permanecerão quando as referências externas do programa (conhecidas como "raízes") forem tiradas. As referências de referência individuais nunca chegam a zero, e os objetos nunca são excluídos, dando-nos um vazamento de memória! A Microsoft lida com este problema dizendo-lhe, ao criar classes COM no VB, apenas para garantir que você nunca tenha referências cíclicas! Se você fizer isso, então os vazamentos de memória subseqüentes são inteiramente sua própria culpa, aparentemente. Pessoalmente, considero que as referências cíclicas são úteis e rejeitam a restrição. Você pode, é claro, apenas quebrar manualmente os ciclos em pontos estratégicos para evitar vazamentos, mas uma solução melhor é ter uma coleta de lixo mais sofisticada. A Microsoft pensa claramente: a coleção de lixo .NET é perfeitamente capaz de lidar com referências cíclicas.
Isso nos leva a uma coisa em favor do COM over .NET: no COM, não há necessidade de implementar AddRef e Release da maneira mostrada aqui. São possíveis esquemas mais sofisticados que lidam adequadamente com referências cíclicas (algo, de fato, que foi explorado, se não documentado, pelo autor). No .NET, por outro lado, um objeto "gerenciado" está sujeito ao coletor de lixo da Microsoft, que você pode ou não gostar, e você não pode substituir o seu próprio. No entanto, o esquema assíncrono de varredura de marca que utilizam tem desvantagens, nomeadamente (i) faz com que o processador funcione desnecessariamente difícil e (ii) não se sabe quando um objeto, marcado como lixo, será finalmente excluído, o que desencoraja um de fazer algo substancial em um destruidor.
O primeiro método em IUnknown é para o benefício da própria COM. O ID globalmente exclusivo (GUID) é um identificador de dezesseis bytes que identifica o objeto como COM. O nome do objeto e o nome do programa ou biblioteca que os serviços normalmente são suficientes para identificar o objeto, mas o GUID se destina a ser exclusivo não apenas ao objeto / biblioteca, mas a uma versão específica dele. Ele entra em seu próprio, por exemplo, armazenamento estruturado. Aqui, um arquivo, conhecido como um contêiner, contém um objeto COM que incorporou dentro de outros objetos COM, um aninhamento que pode continuar a profundidade arbitrária. O GUID na cabeça de cada um destes fluxos identifica os bytes que se seguem. A correspondência de GUIDs com o programa / biblioteca / versão que os serviços é uma função executada pelo Registro do Sistema e permite que um aplicativo abra e visualize documentos OLE sem precisar saber o que são. Este é o mecanismo pelo qual, por exemplo, você pode incorporar uma parte de uma planilha do Excel em um documento do Word. O perigo de usar nomes e não GUIDs é que uma nova versão do programa pode nomear os fluxos do mesmo, mas usar um formato de arquivo diferente (embora, depois de ter dito isso, eles poderiam facilmente esquecer de fornecer um novo GUID, mas não vamos espere com isso). QueryInterface é chamado em VB quando ele precisa verificar se o objeto é realmente um objeto de automação OLE (ou seja, herdado de IDispatch, uma subclasse de IUnknown - a ser investigado em seguida), que é quando o objeto é criado através do VB CreateObject ou de seu equivalente. Uma implementação pode ser a seguinte:
const GUID IID_DerivedFromIUnknown = {0x00000000, 0x0000, 0x0000, 0x00, 0x00, 0x0, 0x0, 0x00, 0x00, 0x00, 0x01};
(substitua esses números por um ID exclusivo obtido de GUIDGEN.exe).
HRESULT DerivedFromIUnknown::QueryInterface(const GUID * const riid, void **ppv)
{
if (riid == IID_IUnknown || riid == IID_IDispatch || riid ==
IID_DerivedFromIUnknown)
{
*ppv = this;
AddRef();
return NOERROR;
}
*ppv = NULL;
return ResultFromScode(E_NOINTERFACE);
}
Para verificar se um IUnknown também é um IDispatch, o COM passa no GUID para IDispatch para este método após o objeto ser criado, dando "Erro em tempo de execução" 430 ': Classe não suporta OLE Automation "se uma resposta negativa for dada (claramente, não será aqui). O QueryInterface é útil quando o IDispatch é apenas uma das muitas interfaces que o objeto fornece. Ponteiros para as outras interfaces, implementadas em objetos relacionados, mas separados, também devem ser obtidas invocando esse método com os GUID relevantes.
(ii) Ligação dinâmica
Em COM, o tipo de objeto não precisa ser conhecido até o tempo de execução. A declaração VB
Dim T As Object
identifique T como um objeto que apóia IUnknown (então não precisa nem ser de tipo IDispatch, embora seja feito algum processamento, QueryInterface será chamado para obter um IDispatch interface). Antes de a VB ter conhecimento das bibliotecas de tipos, este era o único tipo de objeto definido pelo usuário que era possível. Isso significava que uma declaração como
x = T.Method(param1, param2)
teve que ser manuseado de uma maneira que não assumiu conhecimento prévio do objeto T. A string "Método" seria passada para o objeto, com a pergunta, "você apoia isso?", Usando o IDispatch método GetIDsOfNames. Se a resposta fosse "sim" uma identificação seria repassada, o que poderia ser alimentado no método Invoke com os parâmetros, se houver, para executar a ação necessária. Para máxima generalidade, os parâmetros serão passados como uma matriz de comprimento variável do polimórfico VARIANT tipos e Invoke método retornaria um VARIANT. Se o método retornasse um valor, então dependia de VB para coagi-lo no tipo da variável a ser atribuída. A atribuição e consulta de membros de "dados" são casos especiais desta. As atribuições
T.Prop = value
value = T.Prop
que define ou obtém a propriedade Prop do objeto simplesmente acionar a propriedade colocar e os métodos de obtenção de propriedade (uma vez que o acesso aos dados internos é apenas através desses métodos, o esconderijo de dados é, portanto, incorporado, uma necessidade em qualquer caso, pois os dados podem residir em um espaço de memória diferente. Isso faz declarações como
List1.Left = 100
possível, que não só atribui a propriedade da borda esquerda do objeto List1 para 100, mas também move o controle).
Aqui está a definição de IDispatch:
struct IDispatch : IUnknown
{
virtual HRESULT GetTypeInfoCount(unsigned int *pctinfo) = 0;
virtual HRESULT GetTypeInfo(unsigned int itinfo, unsigned long lcid, ITypeInfo **pptinfo) = 0;
virtual HRESULT GetIDsOfNames(const GUID * const riid, char *rgszNames, unsigned int cNames, unsigned long lcid, long *rgdispid) = 0;
virtual HRESULT Invoke(long dispidMember, const GUID * const riid, unsigned long
lcid, unsigned short wFlags,
DISPPARAMS *pdispparams, VARIANT *pvarResult,
EXCEPINFO *pexcepinfo, unsigned int *puArgErr) = 0;
};
Os dois primeiros métodos fornecem o link para uma biblioteca de tipos, o equivalente a um arquivo de cabeçalho, a ser explorado em seguida, que contém informações sobre o objeto (eles são o equivalente ao .NET getType (), mas muito mais difícil de usar). Os outros dois fornecem a funcionalidade de invocação do método dinâmico descrita anteriormente. Estes são mais facilmente implementados usando funções OLE DispGetIDsOfNames e DispInvoke que usam a definição do objeto na biblioteca de tipos para dar as respostas apropriadas.
A biblioteca de tipos
A biblioteca de tipos é o mecanismo pelo qual a VB, ou qualquer outro recipiente, conhece objetos definidos pelo usuário. Ele contém as informações aqui que possibilitam declarações do formulário
Dim T As ObjectType
onde ObjectType é uma classe definida na biblioteca de tipos.
O arquivo de origem, com extensão IDL (Interface Description Language) é uma versão estendida de um arquivo de cabeçalho C ++. Isso é compilado em um binário do tipo TLB (Type LiBrary) por um programa chamado MIDL. Um arquivo IDL pode ser incluído no projeto. Fazer com que VB ou VBA esteja ciente disso é, então, uma questão simples de localizar e incluir o TLB na caixa de diálogo Ferramentas / Referências. Se isso for bem sucedido, os objetos serão visíveis no VBA Object Browser.
Para entender a sintaxe de um arquivo IDL, é necessário perceber que as chamadas de procedimento remoto são uma parte essencial do COM. O objeto pode residir em um espaço de memória diferente, ou mesmo em uma máquina diferente. Os ponteiros não podem, portanto, ser enviados. Os ponteiros só aparecem como parâmetros para marcar os pedaços da memória que devem ser enviados através do limite do processo ou para marcar a memória para receber a resposta. Concessões não são feitas para servidores COM (in-process ou DLL servidores) que residem no mesmo espaço de memória. As palavras-chave in e out são aplicados a cada parâmetro em cada método, para indicar se ele deve ser enviado, recebido ou ambos. Existem restrições sobre os tipos. Arrays devem ser de tipo SAFEARRAY que contém informações vinculadas. As cordas são de tipo BSTR, que contém uma contagem de caracteres. O gerenciamento de memória desses dois tipos deve ser feito por COM, que fornece APIs para o efeito. Um ponteiro para outro objeto COM, no entanto, pode ser passado, caso em que, se o objeto for um processo separado, COM instala a maquinaria (uma proxy para o objeto no cliente, e um stub no servidor) para habilitar a manipulação remota. Mais detalhes no apêndice. Uma biblioteca de tipos também pode incluir cadeias de ajuda e referências sensíveis ao contexto para um arquivo de ajuda. Ele também fornece uma alternativa aos arquivos BAS para declarações de função plana em DLLs através da declaração do módulo. Os parâmetros das funções em uma seção de módulo estão, no entanto, sujeitos às mesmas stringências que os métodos de objeto (embora desnecessariamente como a DLL reside no mesmo espaço de memória).
Aqui está o arquivo IDL na amostra de código:
[uuid(C7E9002B-9E7F-43b5-971D-E2539E6039C2), version(1.0), helpstring
("COMDemo: Demo of COM object defined in C++")]
library COMDemo
{
Um uuid é necessário para a biblioteca como um todo para habilitar o OLE para rastreá-lo no Registro do Sistema. O programa GUIDGEN.exe gera essas IDs (supostamente) globais únicas. Quando o TLB de saída é referenciado em VB / VBA, o Object Browser mostrará o nome da biblioteca e a seqüência de ajuda fornecida aqui para auxiliar a identificação. Usamos esse uuid para obter um ponteiro para a biblioteca de tipos via LoadRegTypeLib.
importlib("stdole2.tlb");
Para obter as definições de IDispatch, etc.
[uuid(2BB79939-EE89-4ae0-BF7D-E7FB175A87CF), oleautomation, dual, hidden]
interface SimpleDispatch : IDispatch
{
/* Esses dois métodos são apenas para ocupar dois slots na tabela de funções virtual. Eles nunca serão chamados diretamente por um recipiente ... */
[hidden] HRESULT VirtualDestructor([in]IUnknown *stream); // spurious parameter hides
function from Object Browser
[hidden] IUnknown *IID_This(); // incorrect parameter hides function from Object
Browser
};
Uma seção interface define uma interface inicialmente vinculada - normalmente, mas não necessariamente, uma classe C ++ acessada através da sua tabela de funções virtual. SimpleDispatch foi criado como uma classe base conveniente para outros objetos COM. Como o destruidor é declarado virtual neste nível, ele ocupará um espaço na tabela de função virtual após as funções de IDispatch e precisa ser acomodado. A próxima função também é interna para obter a classe IID. Ambos serão escondidos do cliente. HRESULT é um código de erro OLE padronizado (e não aparecerá no Object Browser). Pode-se aumentar os erros de VB retornando algo diferente do NOERROR. A menos que "On Error" esteja configurado, isso irá interromper a execução do programa VBA e exibir uma caixa de mensagem. Se o parâmetro final tiver o retval do atributo, esse é o valor de retorno do método, ou a propriedade get, e aparece no Object Browser como tal.
// Custom class definitions ...
[uuid(7C8721D6-3D22-48a1-A945-5FF9815C5807), odl, oleautomation, dual,
helpstring("Name/value pair")]
interface ITestObj : SimpleDispatch
{
[propget, helpstring("Name of quantity")] HRESULT Name([out,retval]BSTR *);
[propput] HRESULT Name([in]BSTR);
[propget, id(0), helpstring("Value (default property)")]
HRESULT Value([out, retval]double *);
[propput, id(0)]
HRESULT Value([in]double); [helpstring("square of value")]
HRESULT Square([out,retval]double *square);
};
Aqui, finalmente, é a definição de uma classe COM simples. O atributo oleautomação combinado com a derivação (embora indireta) do IDispatch torna a interface visível para o VB. O atributo dual indica que os métodos podem ser chamados diretamente usando ligação antecipada, ou indiretamente usando ligação tardia (usando IDispatch :: Invoke). A classe aqui inclui simplesmente o nome da propriedade de cadeia, uma propriedade numérica Valor e um método quadrado para retornar o quadrado de Valor. Embora existam cinco funções aqui, o VBA Object Browser mostrará apenas duas propriedades e um método (e pode-se tornar propriedades somente leitura, omitido o método propput). O id do atributo (0) torna a propriedade do valor a propriedade "padrão" do objeto que (além de uma declaração Set) é usado quando o nome do objeto é usado sem qualificação.
[uuid(5FC711F1-B9C7-4dcc-8CCC-E39F9E0F7556)]
coclass TestObj
{
interface ITestObj;
};
};
Uma seção Coclass agrupa uma ou mais definições de interface e dispinterface (um dispinterface é semelhante a uma interface, mas só suporta ligação tardia). Existe apenas uma interface aqui, mas, se houver mais de uma, as interfaces devem ser mutuamente acessíveis através do QueryInterface. Além disso, um coclass deve ser creatable, o que significa que deve ser capaz de criar uma instância do objeto usando (em VB):
Dim T As Object
Set T = CreateObject("COMDemo.TestObj")
or, if one has a Type Library
Dim T As COMDemo.TestObj
Set T = New COMDemo.TestObj
or simply
Dim T As New COMDemo.TestObj
O conhecimento de como criar o objeto é armazenado no Registro do Sistema. As etapas são as seguintes:
- o Registro do sistema é procurado pela chave HKEY_CLASSES_ROOT \ App.ObjectType \ CLSID que fornece o identificador exclusivo de 16 bytes para o objeto (App.ObjectType é COMDemo.TestObj aqui). Se nenhum for encontrado, o erro "Erro em tempo de execução" 429 ': servidor OLE Automation não pode criar o objeto "é retornado.
- o registro do sistema é procurado pela chave HKEY_CLASSES_ROOT \ CLSID \ {ID exclusiva de 16 bytes} \ InprocServer32, que fornece um caminho completo para uma DLL se o objeto for tratado por uma biblioteca. Se o caminho estiver incorreto, o erro "Erro de automação OLE; o módulo especificado não pôde ser encontrado" é retornado. Se nenhuma chave existe, assume-se que o objeto é tratado em um processo separado, com a comunicação através de chamadas de procedimento remoto. A chave HKEY_CLASSES_ROOT \ CLSID \ {identificação única de 16 bytes} \ LocalServer32 então fornece o nome do caminho completo do executável. Se essa chave não existir, ou o caminho associado não for encontrado, o erro 429 acima é fornecido. Caso contrário, uma chave opcional HKEY_CLASSES_ROOT \ CLSID \ {identificação única de 16 bytes} \ InprocHandler32 identifica o código (o "proxy") que lida com a comunicação RPC no cliente, se o processamento não padrão for necessário aqui.
- Se o objeto for tratado em uma DLL, a DLL será carregada se não estiver na memória, ou uma contagem de referência será adicionada se já estiver na memória. Se for manipulado em um EXE, o executável será carregado, se não estiver executando.
- Para uma DLL, a função DllGetClassObject, que deve ser fornecida e exportada, é chamada com o parâmetro clsid igual ao identificador de classe único de 16 bytes (CLSID) e o parâmetro iid igual a IID_IClassFactory. Espera receber um ponteiro para um objeto derivado de IClassFactory. Se um erro for retornado pela função, VB mostra "Erro de automação OLE: erro não especificado". Para um EXE, espera encontrar um ponteiro para uma fábrica de classe fornecida através de uma chamada para CoRegisterClassObject feita pelo EXE com o mesmo identificador de classe, quando o EXE foi iniciado.
- O método QueryInterface é invocado no ponteiro de fábrica da classe com riid igual a IID_IClassFactory. O ponteiro no segundo parâmetro deve ser feito para apontar para o ponteiro da classe fábrica e a contagem de referência aumentada. Se um erro for enviado novamente, "Erro em tempo de execução" 430 ': A classe não suporta o OLE Automation "é gerada pelo VB. O lançamento é então chamado na fábrica da classe. Para um servidor local, há agora uma série de chamadas de fábrica de classe. Sem entrar em detalhes demais, ele verifica as interfaces IMarshal, IStdMarshalInfo e IExternalConnection, além de mais uma vez que não consegui identificar. Provavelmente, é permitido que um método RPC personalizado seja usado. Se nenhuma interface for retornada em cada caso, o método OLE padrão será usado.
- O método CreateInstance é invocado na fábrica da classe, com NULL e IID_IUnknown passados como os dois primeiros parâmetros. Espera receber um ponteiro IDispatch que se torne o objeto. Se ResultFromScode (E_NOINTERFACE) for retornado, VB mostra o erro 430 acima.
- O método Release é chamado novamente na fábrica da classe.
- O método QueryInterface é chamado no objeto IDispatch com o parâmetro riid igual a IID_IDispatch. O ponteiro no segundo parâmetro deve ser feito para apontar para este apontador e a contagem de referência incrementada. Se um erro for enviado novamente, o erro 430 é gerado pela VB.
- O método de lançamento é chamado no objeto de automação, que agora possui uma contagem de referência de um.
Código de amostra
Um projeto MSL Visual Studio DLL pode ser criado com o anexo COMDemo.idl e COMDemo.cpp. Duas classes utilizáveis são definidas: TestObj e TestWorksheetFuncs, mas outras classes podem ser adicionadas seguindo o mesmo modelo. Após a criação da biblioteca, as classes e a biblioteca de tipos precisam ser registradas usando "regsvr32 COMDemo.dll" no diretório apropriado em um shell de comando com privilégios de administrador. Se mais tarde precisa cancelar o registro, use "regsvr32 / u COMDemo.dll".
Uma vez que o registro TLB e DLL esteja completo, e a biblioteca de tipos é referenciada no VBA, o seguinte deve funcionar:
Dim T As New TestObj
T.Name = "Test 1"
T.Value = 15
Debug.Print T.Name; ": "; T.Value; "squared is"; T.Square
T.Name = "Test 2"
T = 16
Debug.Print T.name; ": "; T; "squared is"; T.Square
O mesmo deve funcionar com ligação tardia sem TLB se T é apenas do tipo Object criado por CreateObject("COMDemo.TestObj").
Nas duas últimas linhas, utilizamos o fato de que o Valor é a propriedade padrão, permitindo que o T seja usado como uma abreviatura para T.Value (exceto em uma declaração Set).
No Microsoft Excel em Developer / Add-ins / Automation, você pode localizar "COMDemo: test worksheet functions" que expõe as duas funções AddTwoNumbers e JoinTwoStrings pela classe TestWorksheetFuncs. Estes são diretamente chamáveis a partir da planilha.
Apêndice: Passando parâmetros para métodos de objeto
Se o parâmetro for designado como "in" no arquivo IDL, ele pode ser lido, mas não modificado. Todo o gerenciamento de memória deve ser deixado ao chamador, o que significa que arrays, strings e objetos COM não devem ser liberados após o uso. Além disso, não deve assumir que o parâmetro continuará a existir assim que o método for encerrado. Se o parâmetro precisa ser retido, então ele deve ser copiado, usando funções de API, como SysAllocString para strings e SafeArrayCopy para arrays. AddRef deve ser chamado em objetos COM que precisam ser mantidos. Se o parâmetro for designado "fora", o destinatário do parâmetro assumirá o gerenciamento de memória. É responsável por liberar cadeias de caracteres e matrizes passadas dessa maneira, e chamar Release em objetos. Um objeto recém-criado deve ser passado com a contagem de referência definida para um. Flout estas regras em seu perigo. Eles são especialmente importantes no caso de passar as referências de objetos através dos limites do processo, porque o objeto recebido / passado é um proxy, um artefato de COM, com uma contagem de referência que é, em certa medida, independente do objeto original. A COM usa as informações na biblioteca de tipos para descobrir como criar o proxy, portanto, se um método não estiver listado, ou não for declarado da maneira correta, então, usá-lo em um cliente remoto trará um desastre. Um proxy também pode ser criado através dos métodos IClassFactory :: CreateInstance e IUnknown :: QueryInterface. É importante notar que este proxy é apenas para o IID passado, então, se o IID for IID_IUnknown, então somente os três métodos IUnknown serão chamáveis no cliente remoto. O IID ao lado da declaração da interface deve ser usado se o acesso a todos os métodos for necessário.