[Calling Pointers]

Introduction

Sunday, August 17, 2003

The problem: How do you call a procedure through a pointer obtained with AddressOf?

The solution: Use C++ and inline assembler code to make the call. Restrict the routine to call to having ByRef Variant arguments only, and do not return a value. Declare the proxy with a variable argument list (ParamArray) in VB.

Caution: these are my first ventures into assembler code, so please read this articles with a grain of salt.

The Code

VB routines and procedure pointers

The callee

The routine called actually changes its ByRef parameters:

' Module1.bas
Option Explicit

Public Sub Foo(ByRef x As Variant, ByRef y As Variant)
    Form1.Caption = Form1.Caption & " " & CStr(x) & "." & CStr(y)
    x = 55
    y = 77
End Sub

Getting the function pointer

You can get the address of any procedure declared in a standard module (provided it's accessible), using classic VB's infamous AddressOf operator. The operator can only be used in a call to another procedure (typically, a Win32 API function). Sometimes, you need to store the function's address somewhere else (for example, in a structure). Then you can use a function that takes a ByVal Long, and returns this argument (as a Long), and call it with the AddressOf operator.

The C++ proxy

The VB signature

The parameter array itself is passed by reference. Moreover, any changes the callee makes to the parameter array's elements are reflected in the variables used for the arguments (this is different in VB.NET, by the way, though the technique here does not apply to VB.NET). Note that VB nests any array arguments into elements of the parameter array - the parameter array will have exactly as many elements as parameters are specified in the call (as an aside, this is different in VB.NET). So here's the Declare:

' Form1.frm
Public Declare Sub FuncLibCall Lib "FuncLib.dll" Alias "Call" ( _
                              ByVal pfn As Long, _
                              ParamArray args() As Variant)

The C++ signature

We need to use the OLE automation type SAFEARRAY, which is the datatype behind classic VB's arrays. We don't bother declaring the pointer variable with a proper function pointer type, because it is going to be called through (typeless) ASM, and the function's signature is not known anyway:

// FuncLib.h
#include <oaidl.h>

#define FUNCLIB_API __declspec(dllexport)

FUNCLIB_API void __stdcall Call(long pfn, SAFEARRAY ** ppsa);

The C++/ASM implementation

So here's the C++ module. I have omitted such tidbits as DllMain, and you'll notice the use of the "stdafx.h" precompiled header:

// FuncLib.cpp
#include "stdafx.h"
#include "FuncLib.h"

#define CHECK(EXP) if(!(EXP)){return;}

FUNCLIB_API void __stdcall Call(long pfn, SAFEARRAY ** ppsa)
{
    HRESULT hres = S_OK;

    // check arguments
    CHECK(pfn != 0);
    CHECK(ppsa != NULL);
    SAFEARRAY * psa = *ppsa;
    CHECK(psa != NULL);

    // element size must be that of Variant (16 Bytes)
    UINT elemSize = SafeArrayGetElemsize(psa);
    CHECK(elemSize == sizeof(VARIANT));

    // only allow 1-dimensional arrays (can't be MD anyway)
    UINT dims = SafeArrayGetDim(psa);
    CHECK(dims == 1);

    // get LBound (should be 0, but who knows about Option Base?)
    long lower = 0;
    hres = SafeArrayGetLBound(psa, 1, &lower);
    CHECK(SUCCEEDED(hres));

    // get upper bound
    long upper = 0;
    hres = SafeArrayGetUBound(psa, 1, &upper);
    CHECK(SUCCEEDED(hres));

    // create an array to copy the arguments to
    VARIANT * pav = new VARIANT[upper - lower + 1];
    CHECK(pav != NULL);

    // copy the argument Variants (we are not supposed to access
    // the SAFEARRAY's data directly)
    for(long i = lower; i <= upper; i++)
    {
        VARIANT v = {0};
        hres =  SafeArrayGetElement(psa, &i, &v);
        CHECK(SUCCEEDED(hres));

        long iZB = (i - lower);
        pav[iZB] = v;
    }

    // save values of certain registers (see VC++ docu)
    long oldESP = 0;
    long oldEBP = 0;
    __asm
    {
        mov oldESP, esp
        mov oldEBP, ebp
    }

    // push the arguments on the stack (note that we must avoid any
    // function calls [like those to the SAFEARRAY API above] while
    // we're setting up the stack for the call)
    for(i = upper; i >= lower; i--) // reverse order!
    {
        long iZB = (i - lower);
        VARIANT * pv = (pav + iZB);

        __asm
        {
            mov eax, pv
            push eax
        }
    }

    // make the call
    __asm
    {
        call pfn
    }

    // clear the stack
    for(i = lower; i <= upper; i++)
    {
        __asm
        {
            pop ebx
        }
    }

    // restore register values
    __asm
    {
        mov esp, oldESP
        mov ebp, oldEBP
    }

    // propagate any changes to the ByRef parameters
    for(i = lower; i <= upper; i++)
    {
        long iZB = (i - lower);
        VARIANT * pv = (pav + iZB);

        hres =  SafeArrayPutElement(psa, &i, pv);
        CHECK(SUCCEEDED(hres));
    }

    // free the argument Variants' copy
    delete[] pav;

}

Note that error handling isn't quite perfect here (with regard to memory cleanup).

The Call

We call the wrapper with variables, so we can watch any changes to the parameters that the callee makes:

' Form1.frm
Option Explicit

Private Sub Form_Load()
    Me.Caption = ""
End Sub

Private Sub Form_DblClick()
    Dim x As Long: x = 5
    Dim y As Long: y = 7
    FuncLibCall AddressOf Module1.Foo, x, y
    Me.Caption = Me.Caption & " " & x & "." & y
End Sub

After the double click, the form should look like this:

.NET?

In .NET languages, if you happen to need to call a raw function pointer (which should rarely be the case, since the AddressOf operator does not return a raw address anymore, but a delegate; and APIs called through P/Invoke likewise return delegates if appropriately declared), use a C++ managed extension. The low-level approach discussed here is would be an unnecessarily hazardous choice.

Earlier, I claimed that you could "declare an appropriate delegate type, and then create a delegate instance reflectively, using the same techniques as shown in the Late Events and Property Delegates articles". Alas, this doesn't work. When instantiating the delegate, the runtime is a bit picky about the function pointer, and tells you that the "function pointer is not created by a delegate". Therefore, you have to use C++'s CLI binding to call native function pointers (such as those obtained with GetProcAddress), or use P/Invoke, declaring the external functions that you use to obtain the function pointer in such a way that the runtime creates a delegate for you.