Marshal 2D array from VB6 to .NET - c#

I have a VB6 assembly which I need to use in my .NET application and generated the Interop DLL for usage with .NET via tlbimp.exe.
The VB6 assembly has a function that has a byref array parameter. I don't want to change anything in the VB6 assembly, so I hope there is a solution to get the following working.
It is filling the array and I want to use it in my .NET code (c# or vb.net).
Example of the VB6 function (file NativeClass.cls):
Public Function GetData(ByRef data() As String) As Integer
Dim tResults() As String
Dim sRecordCount As String
Dim lCount As Long
' load data
sRecordCount = dataDummyObject.RecordCount
ReDim tResults(sRecordCount, 2)
' fill the array in a loop
For lCount = 0 To sRecordCount - 1
tResults(lCount, 0) = dataDummyObject.Fields("property1")
tResults(lCount, 1) = dataDummyObject.Fields("property2")
If (sRecordCount - 1 - lCount) > 0 Then
Call dataDummyObject.MoveNext
End If
End For
data = tResults
GetData = sRecordCount
End Function
Now I want to use it from VB.NET:
Private _nativeAssembly As New NativeClass()
Public Function GetDataFromNativeAssembly() As String()
Dim loadedData As String() = Nothing
_nativeAssembly.GetData(loadedData)
Return loadedData
End Function
C# version:
private NativeClass _nativeAssembly = null;
public string[] GetDataFromNativeAssembly()
{
string[] loadedData = null;
_nativeAssembly.GetData(loadedData);
return loadedData;
}
But when executing the code I get following Exception:
System.Runtime.InteropServices.SafeArrayRankMismatchException: SafeArray of rank 2 has been passed to a method expecting an array of rank 1.
I really need help to solve this problem! Thanks for any piece of advice!

I don't think you can solve this without modifying the VB6 code. Try declaring the function as
Public Function GetData(ByRef data As Variant) As Integer
or
Public Function GetData(ByRef data As Object) As Integer
The ReDim to string array should work fine from Variant. I remember doing it like this all the time because of VB6 not letting a 2D array as a parameter.
When inspecting it from .NET you should see the type. I don't have a VB6 IDE on my machine to verify this.
If one works you should be able to cast over to the String(,) you expect.

This is air code, but you could try this in the VB.Net? Note the additional comma to indicate a 2-D array.
Dim loadedData As String(,) = Nothing

Related

What is the exact equivalent to "KeyValuePair[] dataArray" (C#) in VB?

I need to call a C++ routine with a KeyValuePair parameter. I have an example in C#;
KeyValuePair[] dataArray =
new KeyValuePair[]{
new KeyValuePair() { key = 1234, value = "Atlanta" },
new KeyValuePair() { key = 70, value = "Atlanta" },
new KeyValuePair() { key = 2320, value = "999999999" }};
This seems to translates to;
Dim dataArray As KeyValuePair() = New KeyValuePair() {New KeyValuePair() With {
.key = 1234,
.value = "Atlanta"
}, New KeyValuePair() With {
.key = 70,
.value = "Atlanta"
}}
in VB.
I have two questions;
What is this structure called? I would call it an array of KeyValuePairs. So I know how to reference the structure when I search.
How does one add additional values dynamically?
EDIT:
More specifically I have a function whose purpose is to build the same type of structure statically built in the C# code above by reading values from a database. I had originally written the code assuming the "list of" key value pairs is what I needed. Here is that code;
Private Function buildDataRecord(ByRef objRecord As OracleDataReader) As List(Of KeyValuePair(Of Integer, String))
Dim i As Integer = 0
Dim lstFieldData As List(Of KeyValuePair(Of Integer, String)) = New List(Of KeyValuePair(Of Integer, String))
Dim strFieldName As String
On Error GoTo buildDataRecordError
For i = 0 To objRecord.FieldCount - 1
strFieldName = objRecord.GetName(i)
If strFieldName.Substring(0, 3) = "IN_" _
Then
lstFieldData.Add(New KeyValuePair(Of Integer, String)(CInt(strFieldName.Substring(3)), Trim(objRecord(i).ToString)))
End If
Next
Return lstFieldData
buildDataRecordError:
Err.Raise(Err.Number,
"buildDataRecord",
Err.Description,
Err.HelpFile,
Err.HelpContext)
When calling C++ I get the error;
Cannot marshal 'parameter #4': Generic types cannot be marshaled.
My assumption is I do not have the correct data type.
What is this structure called?
An Array of KeyValuePair. Arrays are CLR types shared by all languages.
How does one add additional values dynamically?
An array is a fixed-size data structure, so you don't just add additional items like you can with a List. But you can resize an array. VB has ReDim, or you can use the generic Array.Resize method that could also be used from C#.
C++ code rely a lot about how your memory is organized for your type.
C# generic type are just describing what the type should contains but nothing about memory organisation. Memory organisation is decided by JIT when it knows the true types of T1 and T2. JIT will probably align Item1 and Item2 on multiple of 8 in memory. It can store Item2 before Item1 (for example if item1 is byte and item2 an int). And in Debug the organisation can be different than in Release. This is why generic are not supported.
You have to define your own struct type for all not generic type versions of KeyValuePair you want to use. And I recommend you to use attribute like this to make thing explicit. Example if you want to handle KeyValuePair<byte, int>
[StructLayout(LayoutKind.Explicit)]
public struct KeyValuePairOfIntAndByte // For KeyValuePair<int, byte>
{
// note how fields are inversed in memory...
// this is the kind of optimisation JIT usually does behind your back
[FieldOffset(0)] public int Item2;
[FieldOffset(sizeof(int))] public byte Item1;
}
So make conversion of your KeyValuePair<int, byte> instance to an array of this struct and that should be ok.
Note that you will have to pin memory while your C++ code is using this C# array too... otherwise Garbage Collector can decide at any time to move your array elsewhere in memory (while your C++ code is using it...)
I strongly recommend you to read this: https://learn.microsoft.com/en-us/dotnet/framework/interop/copying-and-pinning if you want to continue on this way
For information Swig is a library that generate C# code for you that wrap C++ library so you can use C++ library as if it was written in C# (a lot simpler then...) without thinking too much about all the work you have to do. If you look at the generated code you will understand how complex it can be to do interop code right.

Where is the data type mismatch when passing arrays from vba to c#?

My C# COM DLL has a method that accepts a float array and a long int array. It returns a float.
From VBA in an MS Access module, I create an array of type single and another of type long, populate them, create the DLL app.class object and then call its method with the two arrays. But I get a "type mismatch" error.
The following is the actual code, but it is simple because I'm trying to work out the communications before adding the "real" code.
C# code:
public float JustTesting(float[] Array1, long[] Array2)
{
return 96.0F;
}
VBA code:
Public Sub Test()
Dim a1(0 To 0) As Single, a2(0 To 0) As Long, sng As Single
a1(0) = 5
a2(0) = 10
Dim o As Variant
Set o = CreateObject("MyApp.MyClass")
sng = o.JustTesting(a1, a2)
Debug.Print CStr(sng)
Set o = Nothing
End Sub
Where is the data type mismatch?
A Long in VBA is only 32 bits, the same was as an int in C#. So your method needs to take an array of ints
public float JustTesting(float[] Array1, int[] Array2)
{
return 96.0F;
}

How to pass an array (Range.Value) to a native .NET type without looping?

What I am trying to do is populate an ArrayList using .AddRange() method in VBA using late binding on native C# ArrayList, but I can't figure out how to pass an object other than another ArrayList to it as argument... anything else I have tried so far fails...
So basically what I am doing now (Note: list is C#'s ArrayList via mscorlib.dll)
Dim list as Object
Set list = CreateObject("System.Collections.ArrayList")
Dim i As Long
For i = 1 To 5
list.Add Range("A" & i).Value2
Next
But this is quite inefficient and ugly if for example i can be = 500K.
In VBA this also works:
ArrayList1.AddRange ArrayList2
But what I really need/would like is to pass an array instead of ArrayList2
So I heard I can pass an array to the .AddRange() parameter in .NET. I tested it in a small C# console application and it seemed to work just fine. The below works just fine in a pure C# console application.
ArrayList list = new ArrayList();
string[] strArr = new string[1];
strArr[0] = "hello";
list.AddRange(strArr);
So going back to my VBA module trying to do the same it fails..
Dim arr(1) As Variant
arr(0) = "WHY!?"
Dim arrr As Variant
arrr = Range("A1:A5").Value
list.AddRange arr ' Fail
list.AddRange arrr ' Fail
list.AddRange Range("A1:A5").Value ' Fail
Note: I have tried passing a native VBA Array of Variants and Collection, Ranges - everything except another ArrayList failed.
How do I pass a native VBA array as a parameter to an ArrayList?
Or any alternative for creating a collection from Range without looping??
Bonus question: *Or maybe there is another built-in .Net COM Visible Collection that can be populated from VBA Range Object or Array without looping and that already has a .Reverse?
NOTE: I am aware that I can make a .dll wrapper to achieve this but I am interested in native solutions - if any exist.
Update
To better illustrate why I want to completely avoid explicit iteration - here's an example (it uses only one column for simplicity)
Sub Main()
' Initialize the .NET's ArrayList
Dim list As Object
Set list = CreateObject("System.Collections.ArrayList")
' There are two ways to populate the list.
' I selected this one as it's more efficient than a loop on a small set
' For details, see: http://www.dotnetperls.com/array-optimization
list.Add Range("A1")
list.Add Range("A2")
list.Add Range("A3")
list.Add Range("A4")
list.Add Range("A5") ' It's OK with only five values but not with 50K.
' Alternative way to populate the list
' According to the above link this method has a worse performance
'Dim i As Long
'Dim arr2 As Variant
'arr2 = Range("A1:A5").Value2
'For i = 1 To 5
' list.Add arr2(i, 1)
'Next
' Call native ArrayList.Reverse
' Note: no looping required!
list.Reverse
' Get an array from the list
Dim arr As Variant
arr = list.ToArray
' Print reversed to Immediate Window
'Debug.Print Join(arr, Chr(10))
' Print reversed list to spreadsheet starting at B1
Range("B1").Resize(UBound(arr) + 1, 1) = Application.Transpose(arr)
End Sub
Please notice: the only time I have to loop is to populate the list (ArrayList) what I would love to do would be just to find a way to load the arr2 into an ArrayList or another .NET compatible type without loops.
At this point I see that there is no native/built-in way to do so that's why I think I am going to try to implement my own way and maybe if it works out submit an update for the Interop library.
list.AddRange Range("A1:A5").Value
The range's Value gets marshaled as an array. That's about the most basic .NET type you can imagine of course. This one however has bells on, it is not a "normal" .NET array. VBA is a runtime environment that likes to create arrays whose first element starts at index 1. That's a non-conformant array type in .NET, the CLR likes arrays whose first element starts at index 0. The only .NET type you can use for those is the System.Array class.
An extra complication is that the array is a two-dimensional array. That puts the kibosh on your attempts to get them converted to an ArrayList, multi-dimensional arrays don't have an enumerator.
So this code works just fine:
public void AddRange(object arg) {
var arr = (Array)arg;
for (int ix = ar.GetLowerBound(0); ix <= arr2.GetUpperBound(0); ++ix) {
Debug.Print(arr.GetValue(ix, 1).ToString());
}
}
You probably don't care for that too much. You could use a little accessor class that wraps the awkward Array and acts like a vector:
class Vba1DRange : IEnumerable<double> {
private Array arr;
public Vba1DRange(object vba) {
arr = (Array)vba;
}
public double this[int index] {
get { return Convert.ToDouble(arr.GetValue(index + 1, 1)); }
set { arr.SetValue(value, index + 1, 1); }
}
public int Length { get { return arr.GetUpperBound(0); } }
public IEnumerator<double> GetEnumerator() {
int upper = Length;
for (int index = 0; index < upper; ++index)
yield return this[index];
}
System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
return GetEnumerator();
}
Now you can write it the "natural" way:
public void AddRange(object arg) {
var arr = new Vba1DRange(arg);
foreach (double elem in arr) {
Debug.Print(elem.ToString());
}
// or:
for (int ix = 0; ix < arr.Length; ++ix) {
Debug.Print(arr[ix].ToString());
}
// or:
var list = new List<double>(arr);
}
Here's a proof of concept as an expansion of #phoog's comment. As he points out, the AddRange method takes an ICollection.
Implementing ICollection
In the VBA IDE, add a reference to mscorlib.tlb: Tools--->References, then browse to find your .NET Framework mscorlib.tlb. Mine was at "C:\Windows\Microsoft.NET\Framework\vX.X.XXXXX\mscorlib.tlb".
Create a new class called "clsWrapLongArray" as follows:
Option Compare Database
Option Explicit
Implements ICollection
Dim m_lngArr() As Long
Public Sub LoadArray(lngArr() As Long)
m_lngArr = lngArr
End Sub
Private Sub ICollection_CopyTo(ByVal arr As mscorlib.Array, ByVal index As Long)
Dim i As Long
Dim j As Long
j = LBound(m_lngArr)
For i = index To index + (ICollection_Count - 1)
arr.SetValue m_lngArr(j), i
j = j + 1
Next
End Sub
Private Property Get ICollection_Count() As Long
ICollection_Count = UBound(m_lngArr) - LBound(m_lngArr) + 1
End Property
Private Property Get ICollection_IsSynchronized() As Boolean
'Never called for this example, so I'm leaving it blank
End Property
Private Property Get ICollection_SyncRoot() As Variant
'Never called for this example, so I'm leaving it blank
End Property
Here is the Array.SetValue method used.
Create a new module called "mdlMain" to run the example:
Option Compare Database
Option Explicit
Public Sub Main()
Dim arr(0 To 3) As Long
arr(0) = 1
arr(1) = 2
arr(2) = 3
arr(3) = 4
Dim ArrList As ArrayList
Set ArrList = New ArrayList
Dim wrap As clsWrapLongArray
Set wrap = New clsWrapLongArray
wrap.LoadArray arr
ArrList.AddRange wrap
End Sub
If you put a breakpoint on the End Sub and run Main(), you can see by inspecting ArrList in the immediate window that it contains the 4 values added from the Long array. You can also step through the code to see that ArrayList actually calls the ICollection interface members Count and CopyTo to make it happen.
I could see that it might be possible to expand on this idea to create Factory functions to wrap types like Excel Ranges, VBA Arrays, Collections, etc.
This same method works with String arrays! :D
I found some very close answers by Googling "System.ArrayList" "SAFEARRAY" and "System.Array" "SAFEARRAY" and refining with "COM Marshaling", since VBA arrays are COM SAFEARRAYs, but nothing quite explained how one might convert them for use in .NET calls.
It's inefficient because .Value2 is a COM call. Calling it thousands of times adds a lot of interop overhead. Adding items to an array in a loop should be MUCH faster:
Dim list as Object
Set list = CreateObject("System.Collections.ArrayList")
Dim i As Long
Dim a as Variant
a = Range("A1:A5").Value2
For i = 1 To 5
list.Add a(i,1)
Next
How do I pass a native VBA array as a parameter to an ArrayList?
I don't think you can - a native VBA array is not an ICollection, so the only way to create one would be to copy the values in a loop.
The reason it works in a C# console application is because arrays in C# are ICollections, so the AddRange method accepts them just fine.

VB.NET 1D array access using 2 indices

Trying to port over some legacy VB.NET code to C# and running into syntax issues with array indexing.
Within a VB method, called, Get_Coverage_Percentage, is a line of code that looks like this:
Dim RS_Activation_List = Get_Activation_List(Company_ID, Start_Date, End_Date, 0)
where RS_Activation_List was initialized in method Get_Activation_List as
Dim Activation_List(ds.Tables(0).Rows.Count) As Activation_List
Based on my current understanding of VB.NET syntax, that above translated to a 1D array.
However, later in Get_Coverage_Percentage, is a line that accesses Activation_List array as follows:
RS_Prorated_Activation = FormatNumber(RS_Activation_List(RS_Sum_Activation, 5), 2)
How can one be accessing a 1D array using 2 indices? Activation_List itself is a simple structure that is defined as follows:
Structure Activation_List
Dim Mobile_ID As Integer
Dim Radio_Address As Double
Dim Activation_Date As Date
Dim Pro_Rated_Fee As Integer
Dim Sum_Pro_Rated_Fee As Integer
End Structure
VB pro, what am I looking at here?
*Update: *
Method *Get_Activation_List* looks like this, with some parts taken out:
Public Function Iridium_Get_Activation_List(ByVal Company_ID, ByVal Start_Date, ByVal End_Date, ByVal Access_Fee) As Array
Dim vNumOfDaysInMonth = Get_Number_Of_Day_In_Month(Start_Date)
Dim SQL = "SELECT * from assMobileRadio;"
Dim drDataRow As DataRow
Dim ds As DataSet = GetData(SQL)
Iridium_Get_Activation_List = Nothing
Dim Activation_List(ds.Tables(0).Rows.Count) As Activation_List
Dim vSumofProrated = 0
Dim vRow = 0
For Each drDataRow In ds.Tables(0).Rows
'missing code
vRow = vRow + 1
Next
Return Activation_List
End Function
and yes, the code does not compile.
This line is declaring a new variable array of type Activation_List
Dim Activation_List(ds.Tables(0).Rows.Count) As Activation_List
This line declares a different variable of a different type:
Dim RS_Activation_List = Get_Activation_List(Company_ID, Start_Date, End_Date, 0)
We don't know what type this is from your code, but given this line works ok it is a 2d array of some custom type:
RS_Prorated_Activation = FormatNumber(RS_Activation_List(RS_Sum_Activation, 5), 2)
Hover over RS_Activation in VS and see what type it says it is. Also make sure you have Option Strict On.

"Use of unassigned local variable" when trying to pass a struct by reference

I have a VB class library I built from an existing VB class which wraps an unmanaged DLL. The VB class library contains the DLL functions and various structs and types associated with the DLL functions.
I am using the class lib in a C# project and one of the functions in the class lib requires me to pass a struct as an argument. This is where I am running into trouble.
Here is the VB code for the DLL:
Declare Auto Function CtSetVRegister Lib "Ctccom32v2.dll" _
(ByVal ConnectID As Integer, ByRef Storage As CT_VARIANT) As Integer
Here is the VB struct:
<StructLayout(LayoutKind.Sequential, Pack:=1)> _
Public Structure CT_VARIANT
Dim vRegister As Integer 'Variant Register desired
Dim type As Integer 'Format want results returned in
Dim precision As Integer 'Precision desired for floating point conversions
Dim flags As Integer 'Specially defined flags, 0 for normal, (indirection, etc.)
Dim cmd As Integer 'Special commands, 0 for normal operation
Dim taskHandle As Integer 'Alternate task handle for local task register access, 0 = default public
Dim slength As Integer 'Length of bytes returned in stringVar, not include null
Dim indexCol As Integer 'Column (X) selection, base 0
Dim indexRow As Integer 'Row (X) selection base 0
Dim IntegerIntVar As Integer '32 bit signed integer storage
Dim FloatVar As Single '32 bit float
Dim DoubleVar As Double '64 bit double in Microsoft format
<MarshalAs(UnmanagedType.ByValArray, SizeConst:=223)> _
Public stringVar() As Byte 'null terminated ASCII string of bytes (1 to 224)
End Structure
The C# method I am writing requires me to set the necessary values in the struct and then pass those to the DLL function:
private void btnWriteVReg_Click(object sender, System.EventArgs e)
{
int results;
CTC_Lib.Ctccom32v2.CT_VARIANT Var;
Var.vRegister = int.Parse(txtVRegToRead.Text);
Var.cmd = 0;
Var.flags = 0;
Var.FloatVar = 0;
Var.IntegerIntVar = 0;
Var.DoubleVar = 0;
Var.precision = 6;
writeStatus.Text = "";
Var.type = CTC_Lib.Ctccom32v2.CT_VARIANT_INTEGER;
Var.IntegerIntVar = Convert.ToInt32(txtVRegVal.Text);
Var.taskHandle = 0;
results = CTC_Lib.Ctccom32v2.CtSetVRegister(CTconnection,ref Var);
if ((results == SUCCESS))
{
writeStatus.Text = "SUCCESS";
}
else
{
writeStatus.Text = "ERROR";
}
}
I get the error:
Use of unassigned local variable 'Var'
I am a bit puzzled as to how to properly pass the struct 'Var' to the VB Class library.
init variable
CTC_Lib.Ctccom32v2.CT_VARIANT Var = new CTC_Lib.Ctccom32v2.CT_VARIANT();
you must create instance of Var,
CTC_Lib.Ctccom32v2.CT_VARIANT Var = new CTC_Lib.Ctccom32v2.CT_VARIANT();

Categories