Tuesday, 23 October 2018

OLEDB Simple Provider - C++ Sample Step Thru

It is with delight that I can report that I found a OLEDB Simple Provider C++ sample that Microsoft had deposited in Github for posterity. I got it working with a little tweak and was able to write some client VBA ADO code to retrieve an ADO recordset. I also managed to step through the code to document the interaction.

Context - Data Access Solutions

I have been surveying data access solution for working with Excel worksheets. I have found the Microsoft.ACE.OLEDB provider but I have found some weaknesses with it and was wondering what else is possible. As a caveat, it is worth noting Microsoft say they will remove this feature (OLEDB Simple Provider) from a future version of Windows. Nevertheless, for due diligence this solution ought to be explored (later I intend to investigate .NET managed data providers which will be more future proof).

Context - OLEDB Simple Provider VB6 sample

Also note that I have been investigating trying to write a custom implementation for the OLEDB Simple Provider using VBA. VBA is extremely close to VB6. Unfortunately, VB6 has a few extra mechanisms to assist the interaction with the underlying OLEDB Simple Provider interfaces. These extra mechanisms are simply not present in VBA rendering any VB6 code informative but unusable. So I needed to step through the C++ sample because I want to write a C# implementation.

Because a VBA solution is not possible we need to drop down to lower level, C++, so we can figure out how to write a C# implementation.

Client VBA Code

Below is ordinary client VBA ADO code. The key details are in the ADO connection string and the string passed to ADO recordset's Open method.

The connection is critical to correctly using the OLEDB Simple Provider. First, we first specify Provider=MSDAOSP; in all cases. The second term Data Source is where we specify a COM ProgId of a COM class which implements IDataSource. In this case we pass Data Source=ospsampc because ospsampc is the ProgId of the Windows sample. The registry settings can be found in Appendix A .

The other key detail is what is passed to ADO recordset's Open method. In the sample a file path is passed. However, there is no reason why a different implementation may pass a web url or a COM moniker. In the sample code, the file is opened and processed; it is semicolon separated with a row and column count on the top row (see Appendix B).

Option Explicit

'* Tools->References
'ADODB      Microsoft ActiveX Data Objects 6.1 Library  C:\Program Files (x86)\Common Files\System\ado\msado15.dll

Sub TestCustom()
    
    Const sFileName As String = "C:\Users\Simon\source\repos\Windows-class-samples\Samples\Win7Samples\dataaccess\osp\customer.txt"
    Debug.Assert Dir(sFileName) <> ""

    Dim oConn As ADODB.Connection
    Set oConn = New ADODB.Connection
    
    '*
    '* ospsampc is progid of the custom simple provider
    '*
    oConn.Open "Provider=MSDAOSP;Data Source=ospsampc"
    
    Dim rs As ADODB.Recordset
    Set rs = New ADODB.Recordset
    
    Set rs.ActiveConnection = oConn
    
    rs.Open sFileName
    rs.MoveFirst
        
    '* lets get the recordset to a variant array
    Dim vVarArray
    vVarArray = rs.GetRows
End Sub

Tweak to given sample code

So the sample code is at Windows-classic-samples/Samples/Win7Samples/dataaccess/osp/vc at master · Microsoft/Windows-classic-samples · GitHub . It compiles fine for me no problem. One slight tweak is that the output filename needs to be changed. So here are instructions...

  1. Right-click on project icon and from menu select Properties
  2. In Property Pages, select General in the left hand pane
  3. On the right hand pane, change Target Name from $(ProjectName) to ospsampc

Call Graph Walkthrough

The choreography between the classes can be a little confusing so I carefully stepped through the code and annotated it as this is the best way to learn the interaction. What you see below is VBA lines of code interleaved with the C++ sample code that gets called (typically via the OLEDB Simple Provider Dll, msdaosp.dll). Most of it is COM QueryInterface and COM initialisation. I have omitted calls to AddRef, Release and and calls to listening mechanisms as they cluttered the graph.

I recommend reading through the call graph but if you want a summary here it is VBA passes in the conn

  1. VBA passes ADO a connection string to the Connection.Open method, Provider=MSDAOSP;Data Source=ospsampc
  2. ADO sees the Provider=MSDAOSP and knows to load the OLEDB Simple Provider and passes control to its Dll, msdaosp.dll
  3. msdaosp.dll parses the second term of the connection string to find the custom implementation's progid, Data Source=ospsampc
  4. The progid ospsampc is found in the registry and the sample dll, ospsampc.dll, is loaded.
  5. ospsampc.dll is called via entry point DllGetClassObject to get an object that implements IClassFactory.
  6. IClassFactory.CreateInstance is called and ospsampc.dll returns an instance of MyDataSource (whilst creating an instance of MyOSPObject for later).
  7. The instance of MyDataSource is queried for interface IDataSource
  8. Control is passed back to VBA
  9. VBA code creates recordset and sets active connection. No sample code is called for this (must be purely ADO and/or msdaosp.dll).

  10. VBA code passes a filepath as the string parameter to the ADO recordset's Open method.
  11. IDataSource::getDataMember is called with the filepath passed in to bstrDM parameter, this method is implemented by MyDataSource::getDataMember.
  12. MyDataSource::getDataMember calls MyOSPObject::Init on its private instance of MyOSPObject passing the filepath as parameter.
  13. MyOSPObject::Init loads the file into memory
  14. MyDataSource::getDataMember queries MyOSPObject for the OLEDBSimpleProvider interface
  15. The OLEDB Simple provider dll, msdaosp.dll can now call MyOSPObject directly on its OLEDBSimpleProvider interface implementation
  16. msdaosp.dll calls MyOSPObject's OLEDBSimpleProvider interface implementation for row and column count etc as part of initialisation
  17. Control is passed back to VBA
  18. From now on, when VBA calls ADO to manipulate recordset msdaosp.dll makes calls on MyOSPObject's OLEDBSimpleProvider interface implementation.

VBA:    oConn.Open "Provider=MSDAOSP;Data Source=ospsampc"
C:\Program Files (x86)\Common Files\system\ole db\msdaosp.dll

 //
 // get the class factory by calling DllGetClassObject
 //
  ospsampc.dll!DllGetClassObject(const _GUID & rclsid, const _GUID & riid, void * * ppvObj) 
   ospsampc.dll!MyClassFactory::MyClassFactory() 
   ospsampc.dll!MyClassFactory::QueryInterface(const _GUID & riid, void * * ppv) 

 //
 // call CreateInstance on class factory, creates a MyDataSource (and during initialisation internally creates a MyOSPObject)
 // returns an IUnknown , no QI for IDataSource yet
 //
  ospsampc.dll!MyClassFactory::CreateInstance(IUnknown * pUnkOuter, const _GUID & riid, void * * ppv) 
   ospsampc.dll!MyDataSource::MyDataSource() 
  ospsampc.dll!MyDataSource::Init() 
    ospsampc.dll!MyOSPObject::MyOSPObject() 
    ospsampc.dll!CExList::CExList() 
  ospsampc.dll!MyDataSource::QueryInterface(const _GUID & riid, void * * ppv) 


 //
 // After some interim calls to QI for IUnknown we soon get a QI for IDataSource
 //
 ospsampc.dll!MyDataSource::QueryInterface(const _GUID & riid, void * * ppv) 

VBA:    Set rs = New ADODB.Recordset
VBA:    Set rs.ActiveConnection = oConn
VBA:    rs.Open sFileName
C:\Program Files (x86)\Common Files\system\ole db\msdaosp.dll

 //
 // the parameter passed by VBA in the rs.Open method is passed as bstrDM below
 // in this case a file path which gthis implementation will load and process
 // but the string could be an a block of COM moniker pointing to a block of cells on an Excel worksheet
 //
 // after initialization MyOSPObject is QI'ed for OLEDBSimpleProvider interface
 //
 ospsampc.dll!MyDataSource::getDataMember(wchar_t * bstrDM, const _GUID & riid, IUnknown * * ppUnk) 
  ospsampc.dll!MyOSPObject::Init(wchar_t * pwszFilePath) 
  ospsampc.dll!MyOSPObject::QueryInterface(const _GUID & riid, void * * ppv) 


C:\Program Files (x86)\Common Files\system\ole db\msdaosp.dll
 //
 // now msdaosp.dll calls MyOSPObject directly to QI for OLEDBSimpleProvider (again)
 // and then starts calling on OLEDBSimpleProvider::getRowCount , OLEDBSimpleProvider::getColumnCount   etc.
 //
 ospsampc.dll!MyOSPObject::QueryInterface(const _GUID & riid, void * * ppv) Line 87 
 ospsampc.dll!MyOSPObject::getRowCount(long * pcRows) 
 ospsampc.dll!MyOSPObject::getColumnCount(long * pcColumns) 
  ospsampc.dll!MyOSPObject::isAsync(int * pbAsynch)


Links

Appendix A - Registry Entries for sample

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\WOW6432Node\CLSID\{1E79B2C1-077B-11d1-B3AE-00AA00C1A924}]
@="ospsampc"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\WOW6432Node\CLSID\{1E79B2C1-077B-11d1-B3AE-00AA00C1A924}\InprocServer32]
"ThreadingModel"="Both"
@="C:\\Users\\Simon\\source\\repos\\Windows-class-samples\\Samples\\Win7Samples\\dataaccess\\osp\\vc\\Debug\\ospsampc.dll"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\WOW6432Node\CLSID\{1E79B2C1-077B-11d1-B3AE-00AA00C1A924}\ProgID]
@="ospsampc"

[HKEY_LOCAL_MACHINE\SOFTWARE\Classes\WOW6432Node\CLSID\{1E79B2C1-077B-11d1-B3AE-00AA00C1A924}\VersionIndependentProgID]
@="ospsampc"

Appendix B - Sample database file

So the given database file has semicolons separating fields with newline as row terminator. There is also a row and column count on the top row. Also we have a row of column headers. No schema information though. Here at the top rows...

90;10
CustomerID;CompanyName;ContactName;ContactTitle;Address;City;Region;PostalCode;Country;Phone;
ALFKI;Alfreds Futterkiste;Maria Anders;Sales Representative;Obere Str. 57;Berlin;;12209;Germany;030-0074321;
ANATR;Ana Trujillo Emparedados y helados;Ana Trujillo;Owner;Avda. de la Constitución 2222;México D.F.;;05021;Mexico;(5) 555-4729;
ANTON;Antonio Moreno Taquería;Antonio Moreno;Owner;Mataderos  2312;México D.F.;;05023;Mexico;(5) 555-3932;
AROUT;Around the Horn;Thomas Hardy;Sales Representative;120 Hanover Sq.;London;;WA1 1DP;UK;(71) 555-7788;
BERGS;Berglunds snabbköp;Christina Berglund;Order Administrator;Berguvsvägen  8;Luleå;;S-958 22;Sweden;0921-12 34 65;
BLAUS;Blauer See Delikatessen;Hanna Moos;Sales Representative;Forsterstr. 57;Mannheim;;68306;Germany;0621-08460;

No comments:

Post a Comment