10

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.

Community
  • 1
  • 1
ayers
  • 155
  • 7
  • 1
    I understand that you want to get it working first, but editing the IL doesn't seem like a long-term solution. – Eric J. Feb 04 '16 at 00:59
  • Maybe so, but it's the recommended practice for marshaling changes mentioned [here](https://msdn.microsoft.com/en-us/library/ek1fb3c6(v=vs.100).aspx#cpconeditingmicrosoftintermediatelanguagemsilanchor4). FWIW, the /sysarray flag seems to have the same net effect, including the resulting error. – ayers Feb 04 '16 at 01:01
  • You haven't shown how you're declaring the array that you return from VB6 code. Does it have rank 1 and lower bound 0, i.e. declared as something like `Dim msErrors(0 To N) As String`? Also, if mbCacheErrors is false, your current implementation seems to be returning an uninitialized array. – Joe Feb 05 '16 at 14:45
  • I think the lower bound is actually 1, which is the source of the problem. Unfortunately it isn't feasible for me to change the existing VB6 code. – ayers Feb 05 '16 at 19:47
  • 1
    It is very unclear to me what you are doing, you should not be patching a .NET *implementation* of the interface. It is implemented in VB6. Run Tlbimp.exe on the VB6 dll to get the interop library. And do *not* use the `var` keyword on *results*, declare it as System.Array. – Hans Passant Feb 06 '16 at 23:18
  • @HansPassant Thanks. It looks like `var` vs `Array` was the gotcha. I have a vague intuition about why that's the case, but I haven't been able to find details. Can you point to some documentation of why this is the case? – ayers Feb 10 '16 at 00:46

1 Answers1

1

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]);
Mark Bertenshaw
  • 5,594
  • 2
  • 27
  • 40
  • This was definitely the right answer. This IL is exactly what gets produced by TlbImp.exe using the /sysArray flag, however, I think my issues may have been compounded by a couple of things. For anyone else having similar issues, it appears that declaring the result as an `Array` (instead of `var`) was important. I also needed to clear the original assemblies from the GAC and replace them with the new ones. It seems that my original implementation may have been correct at compile time, but IIS at run time used stale assemblies in the GAC. – ayers Feb 10 '16 at 00:47