Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I create a module defined function in a COM Type Library

The VBE7.dll type library used by VBA, has the following MIDL for the Conversion module:

[
  dllname("VBE7.DLL"),
  uuid(36785f40-2bcc-1069-82d6-00dd010edfaa),
  helpcontext(0x000f6ebe)
]
module Conversion {
    [helpcontext(0x000f6ea2)] 
    BSTR _stdcall _B_str_Hex([in] VARIANT* Number);
    [helpcontext(0x000f652a)] 
    VARIANT _stdcall _B_var_Hex([in] VARIANT* Number);
    [helpcontext(0x000f6ea4)] 
    BSTR _stdcall _B_str_Oct([in] VARIANT* Number);
    [helpcontext(0x000f6557)] 
    VARIANT _stdcall _B_var_Oct([in] VARIANT* Number);
    [hidden, helpcontext(0x000f6859)] 
    long _stdcall MacID([in] BSTR Constant);
    [helpcontext(0x000f6ea9)] 
    BSTR _stdcall _B_str_Str([in] VARIANT* Number);
    [helpcontext(0x000f658a)] 
    VARIANT _stdcall _B_var_Str([in] VARIANT* Number);
    [helpcontext(0x000f659f)] 
    double _stdcall Val([in] BSTR String);
    [helpcontext(0x000f64c8)] 
    BSTR _stdcall CStr([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    BYTE _stdcall CByte([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    VARIANT_BOOL _stdcall CBool([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    CY _stdcall CCur([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    DATE _stdcall CDate([in] VARIANT* Expression);
    [helpcontext(0x000f6e7a)] 
    VARIANT _stdcall CVDate([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    short _stdcall CInt([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    long _stdcall CLng([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    int64 _stdcall CLngLng([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    LONG_PTR#i _stdcall CLngPtr([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    float _stdcall CSng([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    double _stdcall CDbl([in] VARIANT* Expression);
    [helpcontext(0x000f64c8)] 
    VARIANT _stdcall CVar([in] VARIANT* Expression);
    [helpcontext(0x000f64b5)] 
    VARIANT _stdcall CVErr([in] VARIANT* Expression);
    [helpcontext(0x000f6c6d)] 
    BSTR _stdcall _B_str_Error([in, optional] VARIANT* ErrorNumber);
    [helpcontext(0x000f6c6d)] 
    VARIANT _stdcall _B_var_Error([in, optional] VARIANT* ErrorNumber);
    [helpcontext(0x000f649b)] 
    VARIANT _stdcall Fix([in] VARIANT* Number);
    [helpcontext(0x000f6533)] 
    VARIANT _stdcall Int([in] VARIANT* Number);
    [helpcontext(0x000f64c8)] 
    HRESULT _stdcall CDec(
        [in] VARIANT* Expression,
        [out, retval] VARIANT* pvar
    );
};

Where I'm particularly interested in how VBA interprets the HRESULT returning CDec function (the last function in the MIDL above), such that within VBA, the CDec function has a signature of

Function CDec(Expression)

It seems like VBA is shadowing the HRESULT returning TLB definition, so to test the theory, I'd like to create my own TLB that defines an HRESULT returning function within a module, and then see how VBA treats that function.

I don't believe this can be done in C# or VB.NET, and when I tried defining a function in a standard module in VB6, the module wasn't visible in the dll.

Is this possible using C++? What sort of project do I need to create? Is there anything special that I need to do? Do I perhaps need to edit the MIDL by hand?

Note: I'm specifically not tagging this question as VBA, as I'm trying to interpret a TLB from C#. In order to test how the VBA host interprets a TLB, I'd like to create an appropriate TLB in any language that supports it. I have Visual Studio 6, 2003, 2013 and 2015 at my disposal.

like image 949
ThunderFrame Avatar asked Jun 13 '17 00:06

ThunderFrame


1 Answers

What's important in CDec declaration is the [out] and [retval] attributes combined. Tools that understand it (like VB/VBA) will be capable of compiling calls to this method in a simplified way, masking error handling, so

HRESULT _stdcall CDec(
        [in] VARIANT* Expression,
        [out, retval] VARIANT* pvar
    );

is equivalent to

VARIANT _stdcall CDec([in] VARIANT* Expression);

equivalent here does not mean it's equivalent in binary form, it just means the tools that understand that syntax will be ok to use (and compile in the final binary target) the first expression when they see the second. It also implies that if there is an error (HRESULT failure) then the tool should raise an error by any way it sees fit (VB/VBA will do this).

That's simply "syntactic sugar".

You can write that using MIDL, but also .NET: just create a standard Class Library using Visual Studio and add this sample c# class:

[ComVisible(true)]
[ClassInterface(ClassInterfaceType.AutoDual)]
public class Class1
{
    public object Test(object obj)
    {
        return obj;
    }
}

Compile that and run the regasm tool to register it, with a command like this:

C:\Windows\Microsoft.NET\Framework64\v4.0.30319\regasm "C:\mypath\ClassLibrary1\bin\Debug\classlibrary1.dll" /tlb /codebase

This will register the class as a COM object, and create a C:\mypath\ClassLibrary1\bin\Debug\classlibrary1.tlb type library file.

Now, start Excel (you could use any COM automation compatible client), and add a reference to the ClassLibrary1 (developer mode, VBA editor, Tools / Reference). If you don't see it you may be running with a different bitness. It's possible to use COM for 32-64 communication, but for now, just make sure your client runs at the same bitness as how your ClassLibrary1.dll was compiled.

Once you have the reference, add some VB code, like this.

Sub Button1_Click()
    Dim c1 As New Class1
    output = c1.Test("hello from VB")
End Sub

As you will experience, VB intellisense will show the method like we expect, like in C#, and it works fine.

Now, let's try to use it from C++: create a console project (again, make sure the bitness is compatible), and add this code to it:

#include "stdafx.h" // needs Windows.h

#import "c:\\Windows\\Microsoft.NET\\Framework64\\v4.0.30319\\mscorlib.tlb" // adapt to your context
#import "C:\\mypath\\ClassLibrary1\\bin\\Debug\\classlibrary1.tlb" 

using namespace ClassLibrary1;

int main()
{
  CoInitialize(NULL);

  _Class1Ptr c1(__uuidof(Class1));
  _variant_t output = c1->Test(L"hello from C++");

  wprintf(L"output: %s\n", V_BSTR(&output));

  CoUninitialize();
  return 0;
}

This will also work fine and the code looks close to VB's one. Notice I used Visual Studio magic #import directive which is super cool because it masks many details of COM Automation plumbing (just like VB/VBA does), including bstr and variant smart classes.

Let's click on the Test call and do a Goto Definition (F12), this is what we see:

inline _variant_t _Class1::Test ( const _variant_t & obj ) {
    VARIANT _result;
    VariantInit(&_result);
    HRESULT _hr = raw_Test(obj, &_result);
    if (FAILED(_hr)) _com_issue_errorex(_hr, this, __uuidof(this));
    return _variant_t(_result, false);
}

haha! This is basically what VB/VBA does also undercovers. We can see how exception handling is done. Again, if you do an F12 on _Class1Ptr, this is what you'll see (simplified):

_Class1 : IDispatch
{
    // Wrapper methods for error-handling

    ...
    _variant_t Test (
        const _variant_t & obj );
    ...

    // Raw methods provided by interface
    ...
      virtual HRESULT __stdcall raw_Test (
        /*[in]*/ VARIANT obj,
        /*[out,retval]*/ VARIANT * pRetVal ) = 0;

};

Here we are. As you can see, the Test method generated by C# in its binary form is of the [out, retval] form as expected. The rest is all sugar and wrappers. Most COM interfaces methods are, at binary level, designed using [out, retval] because compilers don't support a common compatible binary format for function return.

What VBE7 defines is a dispinterface, again some form of syntactic sugar for defining interfaces on top of COM raw/binary IUnknown interface. The only mystery left is why CDec is defined differently than other methods in VBE7. I don't have an answer for that.

Now, specifically about the module keyword in IDL, IDL is just an abstract definitions (functions, constants, classes, etc.) tool that optionally outputs artefacts (.H, .C, .TLB, etc.) targeted for a specific language (C/C++, etc.) or for specific clients.

It happens that VB/VBA supports TLB's constants and methods. It interprets constants as what they are, and functions in modules as DLL exports from the module's dll name.

So if you create this my.idl file somewhere on your disk:

[
    uuid(00001234-0001-0000-0000-012345678901)
]
library MyLib
{   
    [
        uuid(00001234-0002-0000-0000-012345678901),
        dllname("kernel32.dll")
    ]
    module MyModule
    {
        const int MyConst = 1234;

        // note this is the real GetCurrentThreadId from kernel32.dll
        [entry("GetCurrentThreadId")]
        int GetCurrentThreadId();
    }
}

You can compile a TLB from it like this:

midl c:\mypath\my.idl /out c:\mypath

It will create a my.tlb file that you can reference in VB/VBA. Now from VB/VBA, you have a new function available (intellisense will work on it) called GetCurrentThreadId. It works because Windows' kernel32.dll does export a GetCurrentThreadId function.

You can only create DLL Exports from C/C++ projects (and from other languages/tools like Delphi), but not from VB/VBA, not from .NET.

In fact there are some tricks to create exports in .NET, but it's not really standard: Is is possible to export functions from a C# DLL like in VS C++?

like image 160
Simon Mourier Avatar answered Nov 02 '22 06:11

Simon Mourier