Importing a C# .NET 6.0 Core DLL into a C++ without TLB file

Viewed 709

I have quite a number of C# DLLs that were made in .NET 4.6 Framework and need to get upgraded to .NET Core 6 - that part is relatively straightforward.

However, this library ALSO is used within a C++ application, via COM Interop. This option was relatively straightforward in .NET 4.6, because at build time there was an option to expose to COM Interop. This created a .tlb file, which could be imported directly into the C++ app. All of the necessary marshalling was done within the C# code - arrays were passed as SafeArrays, etc. (See example below). Regasm.exe was run after the fact to register the necessary C# code.

With the upgrade to .NET Core 6, the ability to create a .tlb file at runtime was lost. However, (apparently) interoperability should be straightforward. I have seen Nuget packages like DllExport (which has unclear examples for SafeArrays). I've read page after page of Microsoft's references on COM interop and COM hosting, and read relative examples. I briefly looked into the rabbit hole of building my own .tlb file. I've seen many custom examples that do not extend to this exact issue.

Here is some of the code that I am trying to hook together, as an example for you. What kind of changes would I go about making?

C# Code to import

public void CSharpMacro(
  [MarshalAs(UnmanagedType.SafeArray)] double[] D,
  [MarshalAs(UnmanagedType.SafeArray)] double[] O,
  [MarshalAs(UnmanagedType.SafeArray)] double[] H,
  [MarshalAs(UnmanagedType.SafeArray)] double[] L,
  [MarshalAs(UnmanagedType.SafeArray)] double[] C,
  [MarshalAs(UnmanagedType.SafeArray)] double[] V,
  [MarshalAs(UnmanagedType.LPStr)] string FilePath,
  [MarshalAs(UnmanagedType.SafeArray)] ref double[] sOutput,
  [MarshalAs(UnmanagedType.I8)] long CustNum,
  [MarshalAs(UnmanagedType.R8)] double TSDate) 
{
  
  String path = @ "...\Errors.txt";
  
  try 
  {
    //some code
  } 
  catch (Exception e) 
  {
    //error handling
  }
    
}

C++ Importing Example (current method) - Please note, this is a lot of code but the purpose is to show you how the .tlb is imported and how the method from C# is used within the code, and some of what goes on around that, like the use of SafeArrays:

// In the below import statement, use the location of the Release version of your C# DLL 
#import "...\PSP_CSLibrary.tlb" no_namespace

//later in the code... (EasyObject is from external library, don't worry about it)
void far __declspec(dllexport) __stdcall UMacro(EasyObject* pELObj, char* path)
{

    // Initialize the COM interface
    HRESULT hr = CoInitialize(NULL);
    try
    {
        if (SUCCEEDED(hr) && pELObj != NULL)
        {
            // Init pointer to C# Library
            PSP_CSLibraryDLLClassPtr p(__uuidof(PSP_CSLibrary));
            if (p != NULL)
            {
                int datanumber = 1;
                double *dOutput = new double[100];
                for (int j = 0; j < 100; j++) dOutput[j] = 0;
                SAFEARRAY* sOutput = doubleToSA(dOutput, 100);
                EN_DATA_STREAM datastream = (datanumber == 1) ? pELObj->DataStream : GetDataStream(datanumber);
                double TSDate = pELObj->DateTimeMD[datastream]->AsDateTime[0];
                long CustomerNumber = pELObj->Platform->CustomerID;
                int length = pELObj->CloseMD[datastream]->BarsBack;
                if (length > 0)
                {
                    double *dDate = new double[length];
                    double *dOpen = new double[length];
                    double *dHigh = new double[length];
                    double *dLow = new double[length];
                    double *dClose = new double[length];
                    double *dVolume = new double[length];
                    // Load the double arrays with LEAST recent 0
                    for (int i = 0; i < length; i++)
                    {
                        dVolume[length - i - 1]     = pELObj->VolumeMD[datastream]->AsDouble[i];
                        dOpen[length - i - 1]       = pELObj->OpenMD[datastream]->AsDouble[i];
                        dHigh[length - i - 1]       = pELObj->HighMD[datastream]->AsDouble[i];
                        dLow[length - i - 1]        = pELObj->LowMD[datastream]->AsDouble[i];
                        dClose[length - i - 1]      = pELObj->CloseMD[datastream]->AsDouble[i];
                        dDate[length - i - 1]       = pELObj->DateTimeMD[datastream]->AsDateTime[i];
                    }
                    // Convert to safe arrays from double arrays
                    SAFEARRAY* sVolume = doubleToSA(dVolume, length);
                    SAFEARRAY* sHigh = doubleToSA(dHigh, length);
                    SAFEARRAY* sOpen = doubleToSA(dOpen, length);
                    SAFEARRAY* sLow = doubleToSA(dLow, length);
                    SAFEARRAY* sClose = doubleToSA(dClose, length);
                    SAFEARRAY* sDate = doubleToSA(dDate, length);


/////IMPORTANT PART
                    if (sOpen != nullptr && sHigh != nullptr && sLow != nullptr && sOutput != nullptr && p != nullptr &&  sClose != nullptr && dOutput != nullptr)
                    {
                        p->CSharpMacro(sDate, sOpen, sHigh, sLow, sClose, sVolume, path, &sOutput, CustomerNumber, TSDate);
                    }

/////
                    // Release memory
                    if (sDate != nullptr)   SafeArrayDestroy(sDate);
                    if (sVolume != nullptr) SafeArrayDestroy(sVolume);
                    if (sOpen != nullptr)   SafeArrayDestroy(sOpen);
                    if (sHigh != nullptr)   SafeArrayDestroy(sHigh);
                    if (sLow != nullptr)    SafeArrayDestroy(sLow);
                    if (sClose != nullptr)  SafeArrayDestroy(sClose);
                    if (sOutput != nullptr) SafeArrayDestroy(sOutput);
                    if (dDate != nullptr)   delete[] dDate;
                    if (dVolume != nullptr) delete[] dVolume;
                    if (dOpen != nullptr)   delete[] dOpen;
                    if (dHigh != nullptr)   delete[] dHigh;
                    if (dLow != nullptr)    delete[] dLow;
                    if (dClose != nullptr)  delete[] dClose;
                    if (dOutput != nullptr) delete[] dOutput;
                }
            }
            p->Release();
        }

    }
    catch (exception &e)
    {
        // did we get it?
        string out_ = "UMacro error= ";
        std::ostringstream strs;
        strs << e.what();
        std::string str = strs.str();
        out_ += str;
        appendLineToFile("PSP_Interface_Errors.txt", out_);
    }
    CoUninitialize();
    return;
}

This interaction, as shown above, works. Does anyone have ANY insight on how to continue to link up these two but with the C# in .NET Core 6? Preferably with minimal effort. I would like to think most of the work is already done, since types ARE marshalled as you can see. How do I import a C# DLL now, without a tlb file?

Edit

As a note, this must be in unmanaged C++. I am currently investigating using a C++/CLR interface layer, and have gotten moderate success. Will update if successful.

Update

Looks like my approach didn't work. The program I'm making this interface for doesn't accept CLR DLLs, even if they are linked/imported by a native C++ DLL. So the code works for native C++, but not for the intended environment, which is a stock trading program. I'm somewhat back to square one.

Note: my suspicion is that the .tlb file worked because it was using COM, rather than trying to bring in a CLR dll. So some form of COM interop, or manually building the .tlb file should work.

2 Answers

COM Interop approach:

  • Try to use dscom tool to generate *.tlb from .NET 6 assembly. Yes, it's a pre-release tool, but your interface don't look too complex, so it should work.

Importing .NET library without COM Interop:

  • Get rid of COM/SafeArray, use only primitive types in method signature and manual marshal array and string parameters.

    Instead of double[] pass two arguments: a pointer to first element and an array size, like this: double* d_ptr, int d_size, then wrap it in Span. If you want to continue using arrays instead of spans, just convert it via span.ToArray()

    Instead of string pass a pointer to first symbol and then use Marshal.PtrToStringAnsi to convert it to a managed string.

    You could learn more about manual marshalling from DllExport docs and tutorial.

    Then use DllExport library or compile native library from C# code via NativeAOT.

[UnmanagedCallersOnly(EntryPoint="CSharpMacro")] // this attribute also prevents from using incompatible non-primitive types
public static void CSharpMacro(
    double* dArray, int dSize,
    double* oArray, int oSize,
    double* hArray, int hSize,
    double* lArray, int lSize,
    double* cArray, int cSize,
    double* vArray, int vSize,
    IntPtr filePath, // string is null-terminated, don't need to pass a length
    double* sArray, int sSize,
    long CustNum,
    double TSDate) 
{
    // manual marshalling
    var D = new Span<double>(dArray, dSize);
    var DArray = D.ToArray(); // just an example how to convert a Span to Array.
    var O = new Span<double>(oArray, oSize);
    var H = new Span<double>(hArray, hSize);
    var L = new Span<double>(lArray, lSize);
    var C = new Span<double>(cArray, cSize);
    var V = new Span<double>(vArray, vSize);
    var SOutput = new Span<double>(sArray, sSize);
    var FilePath = Marshal.PtrToStringAnsi(filePath);

    try 
    {
        //some code
    } 
    catch (Exception e) 
    {
        //error handling
    }
}

Breakthrough! With the help of the people over on the dscom project, I got the tlb to work!! Thanks so much @epeshk and @halfer !

For anyone else: the key thing I forgot was that I needed to register the comhost.dll from the C# project with regsvr32.exe. I also needed to register the tlb.

Lastly, the whole thing, for my 32-bit C++, had to be built in x86 rather than Any CPU to make sure the comhost had the same 32-bit architecture. On dscom they now have a tool for making 32-bit tlbs - they were very helpful!

Related