Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Consuming VB6 string array in C#

I have (legacy) VB6 code that I want to consume from C# code.

This is somewhat similar to this question, but it refers to passing an array from VB6 consuming a C# dll. My problem is the opposite.

In VB, there is an interface in one dll, and an implementation in another.

Interface:

[
  odl,
  uuid(339D3BCB-A11F-4fba-B492-FEBDBC540D6F),
  version(1.0),
  dual,
  nonextensible,
  oleautomation,
      helpstring("Extended Post Interface.")        
]
interface IMyInterface : IDispatch {

    [id(...),helpstring("String array of errors.")]
    HRESULT GetErrors([out, retval] SAFEARRAY(BSTR)* );
};

Implementation (fragment) in cMyImplementationClass:

Private Function IMyInterface_GetErrors() As String()

    If mbCacheErrors Then
        IMyInterface_GetErrors = msErrors
    End If

End Function

I wrapped these 2 dlls with tlbimp.exe and attempt to call the function from C#.

public void UseFoo()
{
    cMyImplementationClass foo;
    ...
    var result = foo.GetErrors();
    ...
}

Calling foo.GetErrors() causes a SafeArrayRankMismatchException. I think this indicates a marshaling problem as described in the Safe Arrays section here.

The recommendation seems to be to use the /sysarray parameter of tlbimp.exe or to manually edit the IL produced, which I tried.

The original IL looks like this:

.method public hidebysig newslot virtual 
    instance string[] 
    marshal( safearray bstr) 
    GetErrors() runtime managed internalcall
{
  .override [My.Interfaces]My.Interface.IMyInterface::GetErrors
} // end of method cImplementationClass::GetErrors

While the updated version is:

.method public hidebysig newslot virtual 
    instance class [mscorlib]System.Array 
    marshal( safearray) 
    GetErrors() runtime managed internalcall
{
  .override [My.Interfaces]My.Interface.IMyInterface::GetErrors
} // end of method cImplementationClass::GetErrors

I made identical function signature changes in both the interface and implementation. This process is described here. However, it doesn't specify a return value in the function (it uses an "in" reference) and also doesn't use an interface. When I run my code and call from C#, I get the error

Method not found: 'System.Array MyDll.cImplementationClass.GetErrors()'.

It seems to be that something is wrong in the IL that I edited, though I don't know where to go from here.

How can I consume this function from C# without changing the VB6 code?

--Edit-- Redefinition of "msErrors", which initializes the private array that gets returned.

ReDim Preserve msErrors(1 To mlErrorCount)

If I understand correctly, the "1" in that means that the array is indexed from 1 instead of 0, which is the cause of the exception I see get thrown.

like image 528
ayers Avatar asked Feb 04 '16 00:02

ayers


1 Answers

I followed all your steps, except using TlbImp.exe. Instead, I directly added the DLLs into the C# project reference. Doing this, I get IL which is a cross between both of the samples you give:

.method public hidebysig newslot abstract virtual 
        instance class [mscorlib]System.Array 
        marshal( safearray bstr) 
        GetErrors() runtime managed internalcall
{
  .custom instance void [mscorlib]System.Runtime.InteropServices.DispIdAttribute::.ctor(int32) = ( 01 00 00 00 03 60 00 00 )                         // .....`..
} // end of method _IMyInterface::GetErrors

I have done the same code as you, and essentially you are assigning to a variable of type Array. Whilst the CLR supports arrays with lower bounds other than 0, AFAIK, no language, even VB.NET, supports it instrinsically in the language.

My test code becomes:

cMyImplementationClass myImpClass = new cMyImplementationClass();
IMyInterface myInterface = myImpClass as IMyInterface;

myImpClass.CacheErrors = true;

// Retrieve the error strings into the Array variable.
Array test = myInterface.GetErrors();

// You can access elements using the GetValue() method, which honours the array's original bounds.
MessageBox.Show(test.GetValue(1) as string);

// Alternatively, if you want to treat this like a standard 1D C# array, you will first have to copy this into a string[].
string[] testCopy = new string[test.GetLength(0)];
test.CopyTo(testCopy, 0);
MessageBox.Show(testCopy[0]);
like image 190
Mark Bertenshaw Avatar answered Nov 03 '22 09:11

Mark Bertenshaw