Friday, 14 April 2017

Throw away Declare Function with IDL for Modules

So I had some grief recently with Declare Function foo Lib "bar" and passing strings but after discovering that they got cast to narrow ANSI strings I found a workaround. The explanation was found at a SO question Passing strings from VBA to C++ DLL. Also found here was the tantalising suggestion that MIDL.exe (Microsoft's IDL compiler) is meant to be used to replace Declare Function. Eagerly I set bounty to draw out a fuller response. But I have managed to crack the problem myself. Here I will show the code.

I knew a thing or two about Interface Definition Language (IDL) from working with Active Template Library (ATL) which is a Microsoft C++ library for building COM components for Automation clients like VBA. I always knew ATL could be used for COM classes but the SO question tipped that there are other keywords in the IDL language that could define functionality outside of classes.

The key is IDL's Module keyword It allows a Module to be defined and functions defined inside the module. Actually, VBA developers are familiar with this construct since many of the VBA functions are defined in Modules:Strings,DateTime, Math etc.

Let's look at some IDL in OLEView.exe. The joined green elipses should demonstrate how the library in OLEView ties in with VBA's Tools References dialog box. Within the VBA's IDL, I have selected the math module as will drill into this in a moment.

The second exhibit is a screenshot of OLEView.exe and Dependency Walker (Depends.exe). Dependency Walker shows a DLL's exported functions and we can see that VBE7.DLL exports a function called rtcAbsVar which has an entry point ordinal of 656. In the IDL you can also see 656 in the entry(656) attribute for the Abs function and so it can be seen this is how we tie "ABS" in VBA code to call into VBE7.DLL's rtcAbsVar.

Also at top of the IDL we can see dllname("VBA6.DLL") and I confess I do not know why this does not say VBE7.DLL, I could not find a file called VBA6.DLL on my machine. No matter, the code I will give works. We will replicate this technique.

I opened Microsoft Visual Studio 2013 with Administrator rights and I created a new C++, ATL Win32 project and I called it IDLForModules.

Then I click Finish and let VS2013 create the default project. I like to compile it fresh just to ensure no problems (if you forgot to open VS2013 with admin rights you'll get an 'unable to register' error). Once compiled happily then we need to rewrite the IDL code. So open IDLForModules.idl from the Solution Explorer.

Change the IDL to the following (you ought to regenerate your own GUIDs)

// IDLForModules.idl : IDL source for IDLForModules
//

// This file will be processed by the MIDL tool to
// produce the type library (IDLForModules.tlb) and marshalling code.

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

[
 helpstring("Idl For Modules"),
 uuid(EA8C8803-2E90-45B1-8B87-2674A9E41DF1),
 version(1.0),
]
library IDLForModulesLib
{
 importlib("stdole2.tlb");

 [
  /* dllname attribute https://msdn.microsoft.com/en-us/library/windows/desktop/aa367099(v=vs.85).aspx */
  dllname("IdlForModules.dll"),
  uuid(4C1884B3-9C24-4B4E-BDF8-C6B2E0D8B695)
 ]
 module Math{
  /* entry attribute https://msdn.microsoft.com/en-us/library/windows/desktop/aa366815(v=vs.85).aspx */
  [entry(656)] /* map function by entry point ordinal */
  Long _stdcall Abs([in] Long Number);
 }
  module Strings{
  [entry("pUpper")] /* map function by entry point name */
  BSTR _stdcall Upper([in] BSTR Number);
 }
};

You may compile again at this point as it should still compile. Next, we will write some C++ code, so open IDLForModules.cpp. At the top, add the following Standard libraries to the list of includes

#include <string>
#include <algorithm>

And then at the bottom of the module paste in the following code

INT32 __stdcall _MyAbs(INT32 Number) {
 return abs(Number);
}

BSTR __stdcall pUpper(BSTR sBstr)
{
 // Get the BSTR into the wonderful world of std::wstrings immediately
 std::wstring sStd(sBstr);

 // Do some "Mordern C++" iterator style op on the string
 std::transform(sStd.begin(), sStd.end(), sStd.begin(), ::toupper);

 // Dig out the char* and pass to create a return BSTR
 return SysAllocString(sStd.c_str());
}

Compile again as code is still valid but not yet complete. Finally open IDLForModules.def from the Solution Explorer and make the code look like this

; MidlForModules.def : Declares the module parameters.

LIBRARY

EXPORTS
 DllCanUnloadNow  PRIVATE
 DllGetClassObject PRIVATE
 DllRegisterServer PRIVATE
 DllUnregisterServer PRIVATE
 DllInstall  PRIVATE
 _MyAbs @656
 pUpper

Code is now complete and should compile. Next is to write a test client, we will create a macro-enabled Workbook called TestClient.xlsm. This should be housed in the same directory as the created Dll. (Please note there is a type library created but in a different place, don't worry for time being). Use "Open Folder in Explorer" right-click menu option in Solution Explorer then go up one directory and down into Debug (so that's "..\Debug" ) and you should find the created IDLForModules.dll, put TestClient.xlsm there.

Inside TestClient.xlsm open the ThisWorkbook module and write the following code

Option Explicit

Private Declare Function LoadLibrary Lib "kernel32" Alias "LoadLibraryA" (ByVal lpLibFileName As String) As Long

Private Sub Workbook_Open()
    '* next line establishes relative position of Dll
    Debug.Assert Dir(ThisWorkbook.Path & "\IDLForModules.dll") = "IDLForModules.dll"
    
    '* next line loads the Dll so we can avoid very long Lib "c:\foo\bar\baz\barry.dll"
    LoadLibrary ThisWorkbook.Path & "\IDLForModules.dll"
    
    '* next go to  Tools References are check "Idl For Modules"
    '* "Idl For Modules" Iis set in the IDL with helpstring("Idl For Modules")
    
End Sub

The above code loads your new Dll upon workbook opening and this helps the system find the Dll. You could hard code the path into the DllName attribute in the IDL (bad idea) or there must be some sort of DLL search path feature but I have not found that yet. Run this code to ensure you have workbook in the right place, i.e. same directory as the Dll.

Then you go to the Tools References dialog box and you check "Idl For Modules", this is the type library created by your project.
Finally, in a new standard module add the following code and then run it. It should work.

Option Explicit

Sub TestAbs()
    Debug.Print IDLForModulesLib.Math.Abs(-5)
End Sub

Sub TestUpper()
    Debug.Print IDLForModulesLib.Strings.Upper("foobar")
End Sub

If you want to step through the code then in the Project Properties set up Excel.exe as the Command and your TestClient.xlsm as a command argument. Same as bottom of this article.

Just to recap what just happened, we created an ATL project that exported some C++ functions and then we wrote some IDL to map some symbols useable in VBA to the C++ functions, so we did not need the same function name. An example is given of mapping by entry point ordinal as found in the DEF file or by name also as found in the DEF file.

So we can now throw away Declare Function foo lib "bar" as a mechanism.

Thursday, 13 April 2017

Look ma, no Registry! Get IClassFactory direct!

A lot of criticism of COM centres on the Registry. The Registry is a single point of failure and a quick Google will yield many articles condemning it. Com servers store a great deal in the Registry but actually it is possible to bypass the Registry completely.

If you know the location of the COM Server Dll then you can load it with LoadLibrary, you can then use GetProcessAddress to get a function pointer to the entry point DllGetClassObject. Calling DllGetClassObject gets you an interface pointer to IClassFactory and then one can call CreateInstance on that interface. You'll need to know the GUID of the CoClass you want to instantiate as well as the GUID of the interface you're requesting.

Some of this can be written in VBA but getting a function pointer and calling on it are solidly C++ tasks. Here I present code which does the above. First the C++ which needs to be housed in a Win32 Dll project with exports (a .Def file).

#include "Objbase.h"

COMCREATEVIACLASSFACTORY_API HRESULT __stdcall ClassFactoryCreateInstance(
 _In_ HMODULE hModule,
 _In_ _GUID *clsiid,
 _In_ _GUID *iid,
 void** itfUnknown)
{
 HRESULT hr = S_OK;

 IClassFactory* pClassFactory;

 // Declare a pointer to the DllGetClassObject function.
 typedef HRESULT(__stdcall *PFNDLLGETCLASSOBJECT)(REFCLSID clsiid, 
  REFIID RIID, void** PPV);

 PFNDLLGETCLASSOBJECT DllGetClassObject =
   (PFNDLLGETCLASSOBJECT)::GetProcAddress(hModule, "DllGetClassObject");

 // Call DllGetClassObject to get a pointer to the class factory.
 hr = DllGetClassObject(*clsiid, *iid, (void**) &pClassFactory);
 if (hr == S_OK)
 {
  // IClassFactory::CreateInstance and IUnknown::Release
  hr = pClassFactory->CreateInstance(NULL, IID_IUnknown, 
   (void**) itfUnknown);

  pClassFactory->Release();
 }
 return hr;

}



COMCREATEVIACLASSFACTORY_API void TestClassFactoryCreateInstance()
{

 HMODULE hModule = 0;
 hModule = LoadLibrary(L"C:\\Windows\\System32\\scrrun.dll");

 _GUID clsiid, iid;
 ::CLSIDFromString(L"{EE09B103-97E0-11CF-978F-00A02463E06F}", &clsiid);
 ::CLSIDFromString(L"{00000000-0000-0000-C000-000000000046}", &iid);

 HRESULT hr = S_OK;
 IUnknown* pUnknown = 0;

 ClassFactoryCreateInstance(hModule,
  &clsiid,
  &iid, (void**) &pUnknown);

}


And some client VBA

Option Explicit

Declare Function ClassFactoryCreateInstance Lib "ComCreateViaClassFactory.dll" _
           (ByVal hModule As Long, _
            ByRef pguidClass As GUID, _
            ByRef pguidInterface As GUID, _
            ByRef itfUnknown As stdole.IUnknown) As Long

Declare Sub TestClassFactoryCreateInstance Lib "ComCreateViaClassFactory.dll" ()

Declare Function LoadLibrary Lib "Kernel32" Alias "LoadLibraryA" _
            (ByVal lpLibFileName As String) As Long

Const IID_IUnknown          As String = "{00000000-0000-0000-C000-000000000046}"
Const IID_IClassFactory     As String = "{00000001-0000-0000-C000-000000000046}"
Const IID_IClassFactory2    As String = "{B196B28F-BAB4-101A-B69C-00AA00341D07}"

Declare Function CLSIDFromString Lib "OLE32" _
    (ByVal lpszCLSID As String, pclsid As GUID) As Long

Type GUID
    Data1 As Long
    Data2 As Integer
    Data3 As Integer
    Data4(7) As Byte
End Type


Sub Test_ClassFactoryCreateInstance()
    
    Debug.Assert Dir(ThisWorkbook.Path & "\ComCreateViaClassFactory.dll") = _
                "ComCreateViaClassFactory.dll"
    Call LoadLibrary(ThisWorkbook.Path & "\ComCreateViaClassFactory.dll")
    
    Dim clsiid As GUID
    Debug.Assert CLSIDFromString(StrConv( _
        "{EE09B103-97E0-11CF-978F-00A02463E06F}", vbUnicode), clsiid) = 0
    
    Dim riid As GUID
    Debug.Assert CLSIDFromString(StrConv(IID_IUnknown, vbUnicode), riid) = 0
    
    Dim hModule As Long
    hModule = LoadLibrary("C:\Windows\System32\scrrun.dll")
    
    Dim itfUnknown As stdole.IUnknown, hr As Long
    hr = ClassFactoryCreateInstance(hModule, clsiid, riid, itfUnknown)
    If hr <> 0 Then Err.Raise hr
    
    Dim oDict As Scripting.Dictionary
    Set oDict = itfUnknown
    oDict.Add "Foo", 2
    oDict.Add "Bar", 3
    Debug.Assert oDict.Keys()(0) = "Foo"
    Debug.Assert oDict.Keys()(1) = "Bar"
    

End Sub

Sub Test_TestClassFactoryCreateInstance()
    Debug.Assert Dir(ThisWorkbook.Path & "\ComCreateViaClassFactory.dll") = _
            "ComCreateViaClassFactory.dll"
    Call LoadLibrary(ThisWorkbook.Path & "\ComCreateViaClassFactory.dll")

    Call TestClassFactoryCreateInstance

End Sub


A nice diagram here shows what we are doing

Easier for a Camel to go through eye of the Needle than to pass strings from VBA to C++ Dll

It is easier for a Camel to go through eye of the needle than to pass strings from VBA to C++ Dll or at least that is how it has felt for the last day or so.

I have found a good StackOverflow question centred on the issue Passing strings from VBA to C++ DLL. The questioner solves the problem in his question and asks for an explanation before supplying his own. Oddly, the questioner's question is upvoted to 6 whilst his answer is downvoted. Anyway, the response from Ben carries a wonderful nugget. The Declare Function foo lib "bar" interface is constrained by Visual Basic 3 compatibility!

So, the strings get marshalled as narrow ANSI strings! As such, for them to be of any use they need to be converted to Unicode and for this we use the A2W conversion macro from Active Template Library (ATL).

I've put together some sample code and it is given below it demonstrates (1) passing a string as an input argument and (2) passing a string as a buffer for returning a string from the Dll.

A run through as to how create the Dll. In Visual Studio create a new C++ Win32 project.

Then in the application wizard select a Dll and also select 'Export symbols'

Click Finish and let Visual Studio create the project skeleton, when it has finished add a Source.def file, do this via New Item and search for "def" in templates, see diagram.

Edit the Source.def file to contain the following

LIBRARY MyDll  
EXPORTS  
   PassingStrings @1

In the PassStrings.cpp make the code like the following

#include <atlbase.h>

PASSSTRINGS_API  void __stdcall PassingStrings(
 LPCSTR pcsStringIn,
 LPCSTR* pcsBufferToWriteOut,
 INT* charsWritten
 )
{
 /* First Argument, using a string passed in
  * To do something useful with the string you need to convert to a wide
  * the following code demonstrates a dll filepath being loaded
  */
 USES_CONVERSION;
 LPCWSTR w = A2W(pcsStringIn);
 HMODULE hModule = 0;
 hModule = ::LoadLibrary(w);

 /* Second Argument, we can write to the buffer allocated by caller
  * We can use 3rd argument charsWritten to signal how many we've 
  * used or extra will need
  */
 LPCSTR csBufferToWriteOut = *pcsBufferToWriteOut;
 LPCSTR csCreatedInCPlusPlus = LPCSTR(W2A(L"Created in C++"));

 int lenArgIn = strlen(csBufferToWriteOut);
 int lenArgOut = strlen(csCreatedInCPlusPlus);
 if (lenArgIn < lenArgOut)
 {
  // we got a problem, then buffer is not big enough
  // signal size of the gap via *charsWritten
  *charsWritten = lenArgIn - lenArgOut;
 }
 else
 {
  // need to use the following because older functions throw security compilation errors
  strncpy_s((char *) csBufferToWriteOut, lenArgIn, csCreatedInCPlusPlus, lenArgOut);

  *charsWritten = lenArgOut;
 }
}


Where PASSSTRINGS_API is a macro that flips between import and export, this is created by Visual Studio at project creation and will be different for you and will in fact be based on the name of the project.

#ifdef PASSSTRINGS_EXPORTS
#define PASSSTRINGS_API __declspec(dllexport)
#else
#define PASSSTRINGS_API __declspec(dllimport)
#endif

Build the project with F7. Resolve any compilations errors like typos. When built ok proceed to test client.

You'll need a VBA Client, we'll use a macro-enabled Excel workbook, the best location wouild be same directory as the built dll, thjis will be in the Debug folder. To get to the Debug folder from the Solution Explorer right-click menu take 'Open Folder in File Explorer', go up one folder and down into Debug. There create a macro-enabled Excel workbook called TestClient.xlsm.
In TestClient.xlsm add the following code to either ThisWorkbook or a new standard module.

Option Explicit

Private Declare Sub PassingStrings Lib "PassStrings.dll" ( _
        ByVal pcsStringIn As String, _
        ByRef pcsBufferToWriteOut As String, _
        ByRef charsWritten As Long)

Private Declare Function LoadLibrary Lib "Kernel32" Alias "LoadLibraryA" _
(ByVal lpLibFileName As String) As Long


Sub TestPassingStrings()
    '* ensures workbook in the right place
    Debug.Assert Dir(ThisWorkbook.Path & "\PassStrings.dll") = "PassStrings.dll"
    
    '* calling LoadLibrary to manually load library allows our Declare
    '* Function statement to skip the full path to the dll
    '* also allows flexibility in deployment
    Call LoadLibrary(ThisWorkbook.Path & "\PassingStrings.dll")
    
    '* and now the client logic
    Dim sFilePath As String, sBuffer As String, lCharsWritten As Long
    
    sFilePath = "C:\Windows\System32\scrrun.dll"
    
    '* create a buffer for the C++ to write into
    '* it is important we create it and will will free it
    sBuffer = String(20, " ")
    lCharsWritten = 0
    
    '* call the Dll
    Call PassingStrings(sFilePath, sBuffer, lCharsWritten)
    
    '* either buffer was big enough and lCharsWritten > 0
    '* or not and lCharsWritten < 0
    If lCharsWritten > 0 Then
        Debug.Print "'" & Left$(sBuffer, lCharsWritten) & "'"
    Else
        Debug.Print _
         "You need to dimension a buffer larger by '" & Abs(lCharsWritten) & "'"
    End If
End Sub


The code should run. You can play with the size of the buffer to test the buffer overrun defence.

To debug and step though the C++ code you'll need to set the startup program and command line arguments thus.


Now set a break point on the first line of code, switch to the VBA and run the VBA, the C++ break point should hit. At this point you can see the strings passed in in the Locals/Autos window.

Saturday, 8 April 2017

CreateObjectEx VBA to create remote COM object

In my library are some venerable COM books, an excellent tome is by Guy and Henry Eddon, called Inside Distributed COM ISBN 1-57231-849-X.  This has undergone some revisions and was last retitled Inside COM+ Base Services.  There are precious few examples of Visual Basic code, but here is a beauty that allows the creation of a COM server on a remote machine, a capability that C++ developers have always had but which is not easily achieved in VB/VBA.

Enjoy some vintage 1998 code, published under fair usage provisions.




Option Explicit

'* From Inside Distributed DCOM
'* By Guy Eddon and Henry Eddon
'* Copyright 1998 By Guy Eddon and Henry Eddon
'* ISBN 1-57231-849-X
'* Microsoft Press
'* https://www.amazon.co.uk/Inside-Distributed-COM-Guy-Eddon/dp/157231849X

'* Part I: Fudamental Programming Architecture
'* Chapter Three: Type Libraries and Language Integration
'* pages 106-108

'* excerpt published under fair usage provisions

Private Type GUID
    Data1 As Long
    Data2 As Integer
    Data3 As Integer
    Data4(7) As Byte
End Type

Private Type COSERVERINFO
    dwReserved1 As Long  ' DWORD
    pwszName As Long     ' LPWSTR
    pAuthorInfo As Long  ' COAUTHINFO
    dwReserved2 As Long  ' DWORD
End Type

Private Type MULTI_QI
    piid As Long    ' const IID+
    pitf As Object  ' IUnknown
    hr As Long      ' HRESULT
End Type

Enum CLSCTX
    CLSCTX_INPROC_SERVER = 1
    CLSCTX_INPROC_HANDLER = 2
    CLSCTX_LOCAL_SERVER = 4
    CLSCTX_REMOTE_SERVER = 16
    CLSCTX_SERVER = CLSCTX_INPROC_SERVER + CLSCTX_LOCAL_SERVER + _
                CLSCTX_REMOTE_SERVER
    
    CLSCTX_ALL = CLSCTX_INPROC_SERVER + CLSCTX_INPROC_HANDLER + _
            CLSCTX_LOCAL_SERVER + CLSCTX_REMOTE_SERVER
End Enum

Private Const GMEM_FIXED = &H0
Private Const IID_IDispatch As String = "{00020400-0000-0000-C000-000000000046}"
Private Declare Function GlobalAlloc Lib "kernel32" _
    (ByVal wFlags As Long, ByVal dwBytes As Long) As Long
Private Declare Function GlobalFree Lib "kernel32" _
    (ByVal hMem As Long) As Long
Private Declare Function IIDFromString Lib "OLE32" _
    (ByVal lpszIID As String, ByVal piid As Long) As Long
Private Declare Function CLSIDFromString Lib "OLE32" _
    (ByVal lpszCLSID As String, pclsid As GUID) As Long
Private Declare Function CLSIDFromProgID Lib "OLE32" _
    (ByVal lpszProgID As String, pclsid As GUID) As Long
Private Declare Function CoCreateInstanceEx Lib "OLE32" _
    (rclsid As GUID, ByVal pUnkOuter As Long, ByVal dwClsContext As Long, _
    pServerInfo As COSERVERINFO, ByVal cmq As Long, _
    rgmqResults As MULTI_QI) As Long
Private Declare Function lstrcpyW Lib "kernel32" _
    (ByVal lpString1 As String, ByVal lpString2 As String) As Long
    
Public Function CreateObjectEx(ByVal Class As String, _
    Optional ByVal RemoteServerName As String = "") As Object
    
    Dim rclsid As GUID
    Dim hr As Long
    Dim ServerInfo As COSERVERINFO
    Dim Context As Long
    Dim mqi As MULTI_QI
    
    mqi.piid = GlobalAlloc(GMEM_FIXED, 16)
    ' Convert the string version of IID_Dispatch to a binary IID.
    hr = IIDFromString(StrConv(IID_IDispatch, vbUnicode), mqi.piid)
    If hr <> 0 Then Err.Raise hr
        
    ' Convert the CLSID or ProgID string to a binary CLSID.
    If ((Left(Class, 1) = "{") And (Right(Class, 1) = "}") And _
        (Len(Class) = 38)) Then
        ' Create a binary CLSID from string representation
        hr = CLSIDFromString(StrConv(Class, vbUnicode), rclsid)
        If hr <> 0 Then Err.Raise hr
    Else
        ' Create a binry CLSID from a ProgID string.
        hr = CLSIDFromProgID(StrConv(Class, vbUnicode), rclsid)
        If hr <> 0 Then Err.Raise hr
    End If

    ' Set up the class context
    If RemoteServerName = "" Then
        Context = CLSCTX_SERVER
    Else
        Context = CLSCTX_REMOTE_SERVER
        Dim MachineArray() As Byte
        ReDim MachineArray(Len(StrConv(RemoteServerName, _
            vbUnicode)) + 1)
        ServerInfo.pwszName = lstrcpyW(MachineArray, _
                StrConv(RemoteServerName, vbUnicode))
    End If

    
    ' Create the object
    hr = CoCreateInstanceEx(rclsid, 0, Context, ServerInfo, 1, mqi)
    If hr <> 0 Then Err.Raise hr
    GlobalFree mqi.piid
    Set CreateObjectEx = mqi.pitf
End Function

Sub TestCreateObjectEx()
    Dim obj(0 To 1) As Object
    Set obj(0) = CreateObjectEx("Scripting.Dictionary")
    Debug.Assert TypeName(obj(0)) = "Dictionary"
    Set obj(1) = CreateObjectEx("{EE09B103-97E0-11CF-978F-00A02463E06F}")
    Debug.Assert TypeName(obj(1)) = "Dictionary"
    
End Sub


I only have one machine at the moment and cannot test the remoting functionality. But this shows good use of COM System functions

Use Shell to give you multitasking

Often on StackOverflow I see questions regarding long-running file handling using Scripting Runtime.  They really should learn to shell out to the command line, this way their VBA code does block.

So the simplest case is Shell and Forget here is some code.  You need to read the ComSpec environment variable and you need to pass the command switches /S /C.

Here we are opening the workbook's home folder in Windows Explorer.



Sub ExploreMyHomeDir()
    Shell (Environ("comspec") & " /s /c explorer.exe " & ThisWorkbook.Path)
End Sub

Tuesday, 4 April 2017

COM System Functions

So COM is complex and unfortantely there are many toolkits and layers that are designed to help but actually can obscure understanding.  Sometimes, it is better to know the underlying mechanism and in COM so very often this means calling a System function.  These system functions are exported by well know COM dlls like OLE32.DLL and OLEAUT32.DLL.

On this page we show all the functions that these DLLs export so they can be browsed.

Array Manipulation Functions Decimal Arithmetic Functions String Manipulation Functions
SafeArrayAccessData VarDecAbs SysAddRefString
SafeArrayAddRef VarDecAdd SysAllocString
SafeArrayAllocData VarDecCmp SysAllocStringByteLen
SafeArrayAllocDescriptor VarDecCmpR8 SysAllocStringLen
SafeArrayAllocDescriptorEx VarDecDiv SysFreeString
SafeArrayCopy VarDecFix SysReAllocString
SafeArrayCopyData VarDecInt SysReAllocStringLen
SafeArrayCreate VarDecMul SysReleaseString
SafeArrayCreateEx VarDecNeg SysStringByteLen
SafeArrayCreateVector VarDecRound SysStringLen
SafeArrayCreateVectorEx VarDecSub Type Building Functions
SafeArrayDestroy Dispatch functions CreateTypeLib
SafeArrayDestroyData CreateDispTypeInfo CreateTypeLib2
SafeArrayDestroyDescriptor CreateStdDispatch OaBuildVersion
SafeArrayGetDim DispCallFunc Type Description Functions
SafeArrayGetElement DispGetIDsOfNames LHashValOfNameSys
SafeArrayGetElemsize DispGetParam LHashValOfNameSysA
SafeArrayGetIID DispInvoke LoadRegTypeLib
SafeArrayGetLBound Error-Handling Functions LoadTypeLib
SafeArrayGetRecordInfo CreateErrorInfo LoadTypeLibEx
SafeArrayGetUBound GetErrorInfo OaEnablePerUserTLibRegistration
SafeArrayGetVartype SetErrorInfo QueryPathOfRegTypeLib
SafeArrayLock Formatting Functions RegisterTypeLib
SafeArrayPtrOfIndex VarFormat RegisterTypeLibForUser
SafeArrayPutElement VarFormatCurrency UnRegisterTypeLib
SafeArrayRedim VarFormatDateTime UnRegisterTypeLibForUser
SafeArrayReleaseData VarFormatFromTokens UDT Functions and Interfaces
SafeArrayReleaseDescriptor VarFormatNumber ClearCustData
SafeArraySetIID VarFormatPercent GetRecordInfoFromGuids
SafeArraySetRecordInfo VarMonthName GetRecordInfoFromTypeInfo
SafeArrayUnaccessData VarTokenizeFormatString Variant Arithmetic Functions
SafeArrayUnlock VarWeekdayName VarAbs
BSTR and Vector Conversion Functions Numeric Parsing Functions VarAdd
BstrFromVector VarNumFromParseNum VarAnd
SetOaNoCache VarParseNumFromStr VarCat
VarBstrCat OLE and Data Transfer VarCmp
VarBstrCmp OleCreateFontIndirect VarDiv
VectorFromBstr OleCreatePictureIndirect VarEqv
COM Fundamentals OleLoadPicture VarFix
DllCanUnloadNow OleLoadPictureEx VarIdiv
DllGetClassObject Picture Functions VarImp
DllRegisterServer OleLoadPictureFile VarInt
DllUnregisterServer OleLoadPictureFileEx VarMod
OleIconToCursor OleSavePictureFile VarMul
Controls and Property Pages Registration Functions VarNeg
OleCreatePropertyFrame GetActiveObject VarNot
OleCreatePropertyFrameIndirect RegisterActiveObject VarOr
OleLoadPicturePath RevokeActiveObject VarPow
OleTranslateColor RPC Data Marshaling Functions VarR4CmpR8
Currency Arithmetic Functions BSTR_UserFree VarR8Pow
VarCyAbs BSTR_UserFree64 VarR8Round
VarCyAdd BSTR_UserMarshal VarRound
VarCyCmp BSTR_UserMarshal64 VarSub
VarCyCmpR8 BSTR_UserSize VarXor
VarCyFix BSTR_UserSize64 Variant Manipulation Functions
VarCyInt BSTR_UserUnmarshal VariantChangeType
VarCyMul BSTR_UserUnmarshal64 VariantChangeTypeEx
VarCyMulI4 VARIANT_UserUnmarshal64 VariantClear
VarCyMulI8 VARIANT_UserUnmarshal VariantCopy
VarCyNeg VARIANT_UserSize64 VariantCopyInd
VarCyRound VARIANT_UserSize VariantInit
VarCySub VARIANT_UserMarshal64 (not documented)
Date and Time Conversion Functions VARIANT_UserMarshal OACreateTypeLib2
DosDateTimeToVariantTime VARIANT_UserFree64 OACleanup
GetAltMonthNames VARIANT_UserFree HWND_UserUnmarshal64
SystemTimeToVariantTime LPSAFEARRAY_UserUnmarshal64 HWND_UserUnmarshal
VarDateFromUdate LPSAFEARRAY_UserUnmarshal HWND_UserSize64
VarDateFromUdateEx LPSAFEARRAY_UserSize64 HWND_UserSize
VariantTimeToDosDateTime LPSAFEARRAY_UserSize HWND_UserMarshal64
VariantTimeToSystemTime LPSAFEARRAY_UserMarshal64 HWND_UserMarshal
VarUdateFromDate LPSAFEARRAY_UserMarshal HWND_UserFree64
LPSAFEARRAY_UserFree64 HWND_UserFree
LPSAFEARRAY_UserFree
LPSAFEARRAY_Unmarshal
LPSAFEARRAY_Size
LPSAFEARRAY_Marshal

What is also exported from OleAut32.dll is a whole number of Variant conversion functions which are best presented cross-tabulated.

Dec Bool Bstr Cy Date Disp I1 I2 I4 I8 R4 R8 UI1 UI2 UI4 UI8
Dec # # # # # # # # # # # # # # #
Bool # # # # # # # # # # # # # # #
Bstr # # # # # # # # # # # # # # #
Cy # # # # # # # # # # # # # # #
Date # # # # # # # # # # # # # # #
I1 # # # # # # # # # # # # # # #
I2 # # # # # # # # # # # # # # #
I4 # # # # # # # # # # # # # # #
I8 # # # # # # # # # # # # # #
R4 # # # # # # # # # # # # # # #
R8 # # # # # # # # # # # # # # #
UI1 # # # # # # # # # # # # # # #
UI2 # # # # # # # # # # # # # # #
UI4 # # # # # # # # # # # # # # #
UI8 # # # # # # # # # # # # # #

So another nice dll is OleDlg.dll which seems to be a repository of OLE and Data Transfer dialogs. Most of the entry points are duplicated with A meaning Windows 98 and W meaning Windows 2000 onwards.

oleDlg.dll
OleUIAddVerbMenuA OleUIAddVerbMenuW
OleUIBusyA OleUIBusyW
OleUICanConvertOrActivateAs
OleUIChangeIconA OleUIChangeIconW
OleUIChangeSourceA OleUIChangeSourceW
OleUIConvertA OleUIConvertW
OleUIEditLinksA OleUIEditLinksW
OleUIInsertObjectA OleUIInsertObjectW
OleUIObjectPropertiesA OleUIObjectPropertiesW
OleUIPasteSpecialA OleUIPasteSpecialW
OleUIPromptUserA OleUIPromptUserW
OleUIUpdateLinksA OleUIUpdateLinksW

And now from OLE32.DLL which is the daddy of all COM System DLLs. Breaking it down first we have the error info functions

Automation - Error Info
CreateErrorInfo GetErrorInfo SetErrorInfo

Also from OLE32.DLL a chunk of funcitons that handle OLE and Data Transfer such as clipboards, links, drag and drop.

OLE and Data Transfer
CreateDataAdviseHolder CreateDataCache CreateOleAdviseHolder
DoDragDrop OleCreate OleCreateDefaultHandler
OleCreateEmbeddingHelper OleCreateEx OleCreateFromData
OleCreateFromDataEx OleCreateFromFile OleCreateFromFileEx
OleCreateLink OleCreateLinkEx OleCreateLinkFromData
OleCreateLinkFromDataEx OleCreateLinkToFile OleCreateLinkToFileEx
OleCreateMenuDescriptor OleCreateStaticFromData OleDestroyMenuDescriptor
OleDraw OleDuplicateData OleFlushClipboard
OleGetClipboard OleInitialize OleIsCurrentClipboard
OleIsRunning OleLoad OleLoadFromStream
OleLockRunning OleMetafilePictFromIconAndLabel OleNoteObjectVisible
OleQueryCreateFromData OleQueryLinkFromData OleRegEnumFormatEtc
OleRegEnumVerbs OleRun OleSave
OleSaveToStream OleSetClipboard OleSetContainedObject
OleSetMenuDescriptor OleTranslateAccelerator OleUIAddVerbMenu
OleUninitialize RegisterDragDrop ReleaseStgMedium
RevokeDragDrop

Also in OLE32.Dll function concerning Structured Storage

Structured Storage
CreateILockBytesOnHGlobal CreateStreamOnHGlobal
FmtIdToPropStgName FreePropVariantArray
GetConvertStg GetHGlobalFromILockBytes
GetHGlobalFromStream OleConvertIStorageToOLESTREAM
OleConvertIStorageToOLESTREAMEx OleConvertOLESTREAMToIStorage
OleConvertOLESTREAMToIStorageEx PropStgNameToFmtId
PropVariantClear PropVariantCopy
ReadClassStg ReadClassStm
ReadFmtUserTypeStg StgConvertPropertyToVariant
SetConvertStg StgConvertVariantToProperty
StgCreateDocfile StgCreateDocfileOnILockBytes
StgCreatePropSetStg StgCreatePropStg
StgCreateStorageEx StgGetIFillLockBytesOnFile
StgGetIFillLockBytesOnILockBytes StgIsStorageFile
StgIsStorageILockBytes StgOpenAsyncDocfileOnIFillLockBytes
StgOpenPropStg StgOpenStorage
StgOpenStorageEx StgOpenStorageOnILockBytes
StgPropertyLengthAsVariant StgSetTimes
WriteClassStg WriteClassStm
WriteFmtUserTypeStg

Finally, the canonical functions described as COM Fundamentals.

COM Fundamentals
BindMoniker CLSIDFromProgID
CLSIDFromProgIDEx CLSIDFromString
CoAddRefServerProcess CoAllowSetForegroundWindow
CoAllowUnmarshalerCLSID CoCancelCall
CoCopyProxy CoCreateFreeThreadedMarshaler
CoCreateGuid CoCreateInstance
CoCreateInstanceEx CoCreateInstanceFromApp
CoDisableCallCancellation CoDisconnectContext
CoDisconnectObject CoDosDateTimeToFileTime
CoEnableCallCancellation CoEnterApplicationThreadLifetimeLoop
CoFileTimeNow CoFileTimeToDosDateTime
CoFreeAllLibraries CoFreeLibrary
CoFreeUnusedLibraries CoFreeUnusedLibrariesEx
CoGetApartmentType CoGetCallContext
CoGetCallerTID CoGetCancelObject
CoGetClassObject CoGetContextToken
CoGetCurrentLogicalThreadId CoGetCurrentProcess
CoGetInstanceFromFile CoGetInstanceFromIStorage
CoGetInterceptor CoGetInterfaceAndReleaseStream
CoGetMalloc CoGetMarshalSizeMax
CoGetObject CoGetObjectContext
CoGetPSClsid CoGetStandardMarshal
CoGetStdMarshalEx CoGetSystemSecurityPermissions
CoGetTreatAsClass CoHandlePriorityEventsFromMessagePump
CoImpersonateClient CoIncrementMTAUsage
CoInitialize CoInitializeEx
CoInitializeSecurity CoInstall
CoInvalidateRemoteMachineBindings CoIsHandlerConnected
CoIsOle1Class CoLoadLibrary
CoLockObjectExternal CoMarshalHresult
CoMarshalInterface CoMarshalInterThreadInterfaceInStream
CoQueryAuthenticationServices CoQueryClientBlanket
CoQueryProxyBlanket CoRegisterActivationFilter
CoRegisterChannelHook CoRegisterClassObject
CoRegisterInitializeSpy CoRegisterMallocSpy
CoRegisterMessageFilter CoRegisterPSClsid
CoRegisterSurrogate CoRegisterSurrogateEx
CoReleaseMarshalData CoReleaseServerProcess
CoResumeClassObjects CoRevertToSelf
CoRevokeClassObject CoRevokeInitializeSpy
CoRevokeMallocSpy CoSetCancelObject
CoSetMessageDispatcher CoSetProxyBlanket
CoSuspendClassObjects CoSwitchCallContext
CoTaskMemAlloc CoTaskMemFree
CoTaskMemRealloc CoTestCancel
CoTreatAsClass CoUninitialize
CoUnmarshalHresult CoUnmarshalInterface
CoWaitForMultipleHandles CoWaitForMultipleObjects
CreateAntiMoniker CreateBindCtx
CreateClassMoniker CreateFileMoniker
CreateGenericComposite CreateItemMoniker
CreateObjrefMoniker CreatePointerMoniker
DllDebugObjectRPCHook DllGetClassObject
DllRegisterServer GetClassFile
GetRunningObjectTable IIDFromString
IsAccelerator IsEqualGUID
MkParseDisplayName MonikerCommonPrefixWith
MonikerRelativePathTo OleDoAutoConvert
OleGetAutoConvert OleGetIconOfClass
OleGetIconOfFile OleRegGetMiscStatus
OleRegGetUserType OleSetAutoConvert
ProgIDFromCLSID StringFromCLSID
StringFromGUID2 StringFromIID