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

1

8

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.

Alister answered 13/6, 2017 at 0:32 Comment(7)
I would assume VBA asserts some sort of exception when the HRESULT value returned indicates an error.Roanne
VBA doesn't expose the function as returning an HRESULT, and accepting 2 parameters. It only permits returning a Variant, and accepting a single parameter. I'm trying to determine whether VBA special cases the CDec function when interpreting the TLB, or whether it handles all similarly defined functions in the same way.Alister
And I told you how I think it special cases the function.Roanne
Note that CDec "123" (no retval assignment) is valid while Fix "123" does not compile -- weird! Also Fix might raise error on invalid input, although no HRESULT is in sight. Also VARIANT is a massive struct for a retval so C/C++ compiler implicitly creates output param for it which comes first in arg list.Abroms
@Abroms yes, Fix is defined by the spec as an Intrinsic function, much like Len, CDbl and others - they must all assign the return value. CDec is not defined as an Intrinsic function. An Instrinsic function can be used in an Enum expression, while CDec cannot.Alister
This may be totally off topic, but your source code is a Python Developer's worst nightmare.Phantasy
Seriously though, this makes Lisp look like a Pizza-Pie-Chocolate-Cake-Lemon-Meringue wrapped in gold leaf paper under the Christmas tree of Santa Christ himself.Phantasy
A
6

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++?

Abrams answered 16/6, 2017 at 6:45 Comment(4)
Thanks. I was aware of the behavior within a CoClass and Interface. I can see, for example, that the _ErrObject interface has HRESULT returning signatures, but the CoClass "wraps them up". My question was specifically around how signatures in modules (not Interfaces/CoClasses) can be created, so that I can test how VBA handles them. Ultimately I'm trying to discover if VBA is special casing CDec or whether it handles all module-defined HRESULT-returning signatures in the same way, but as I can't find any other TLBs that contain a module defined function, I wanted to roll my own.Alister
@Alister - Your mixing a lot of concepts. CoClass don't wrap anything. CoClass is just a definition. Modules define functions, with behavior the same as for interface methods (which are also functions). TLB just contain definitions (metadata, classes, methods), not code. You can roll your own TLB using MIDL, but then what will you do with it? Your question is far from being clear.Abrams
That may be, but the MIDL is clearly distinguishing between functions defined in classes, and functions defined in a "module". My questions was specifically about how to create a function in a module and not within a class, but for all I know, a module-defined function might need to be defined in a class, but decorated to make it appear as a module in the MIDL?Alister
@Alister - I've made an update, I'm still unsure what you're after :-)Abrams

© 2022 - 2024 — McMap. All rights reserved.