C#, Matrici e Interoperability

Creato il 14 febbraio 2014 da Matteo Tosato @MatteoTosato87

Alcuni giorni fa persi alcune ore di lavoro a capire come poter chiamare una funzione esportata da una DLL scritta in codice nativo (C++) verso una applicazione Net framework.

Quando è necessario chiamare una funzione esterna esportata da una DLL C++ è necessario convertirne gli eventuali parametri da codice gestito a non gestito.

Il servizio Interoperability C# non possiede un metodo di marshalling adeguato per gli array multidimensionali. Per la maggior parte dei tipi .Net, l’assembly del framework: System.Runtime.InteropServices.Marshal, possiede metodi che provvedono in maniera automatica alla allocazione di oggetti non gestiti e di conseguenza, alla conversione delle variabili C#.

Quello che segue è un esempio classico,
il codice C++ definisce ed esporta una funzione di libreria:

...
__declspec(dllexport) Mlp* MlpInstantiate(
	uint a,
	uint b,
	uint *c,
	uint d,
	bool use
) { ... }
...


Ove ‘a’, ‘b’, ‘d’ sono varaibili unsigned int, ‘c’ è un array di variabili uint e ‘use’ è un tipo bool.
Per poter esportare la funzione, vi sarà una dichiarazione ‘extern “C”‘ del prototipo all’interno del progetto stesso:

...
extern "C" __declspec(dllexport)
    Mlp* MlpInstantiate(uint a, uint b, uint *c, uint d,bool use);
...

Il tipo di ritorno corrisponde ad un puntatore ad un oggetto.

Dalla parte .Net le variabili non necessitano di particolari attenzioni, ovvero un int, uint, char, etc vengono passati come tali. Per array ed oggetti invece è necessaria la conversione con i metodi della classe Marshal. La corrispettiva dichiarazione della funzione in codice gestito assume perciò la forma:

...
[DllImport("lib.dll", CallingConvention=CallingConvention.Cdecl)]
public unsafe extern static void* MlpInstantiate(
   uint a,
   uint b,
   [MarshalAs(UnmanagedType.LPArray)] uint[] c,
   uint d,
   bool use);
...

Qui, il parametro ‘CallingConvention‘ specifica che verrà fatta la pulizia dello stack, inoltre, stabilisce che in questa funzione è possibile che vi sia un numero variabili di argomenti.
[MarshalAs(UnmanagedType.LPArray)]‘ indica che verrà eseguito il Marshalling, ovvero la conversione da un tipo di array gestito ad un puntatore di un array non gestito.

Ovviamente la funzione sarà di tipo ‘unsafe‘.

Fin qui non c’è nulla di complicato, le Marshal vengono chiamate ogni qual volta abbiamo necessità di questo tipo.

Consideriamo invece la funzione C++ seguente:

...
extern "C" __declspec(dllexport)
    void Training(Mlp* net, double **ArrayA, double **ArrayB, uint input);
...

Non esiste un attributo per convertire le matrici double che dobbiamo passare.
Pertanto è necessario convertire manualmente i due oggetti gestiti in due non gestiti, il tutto all’interno della nostra applicazione C#.

Dopo alcuni tentativi, sono arrivato ad una soluzione funzionante per la procedura di conversione.

La piattaforma di chiamata assume la forma:

...
[DllImport("lib.dll", CallingConvention = CallingConvention.Cdecl)]
public unsafe extern static void Training(
   void* net,
   IntPtr inputPatterns,
   IntPtr outputPatterns,
   uint inputRows);
...

Il processo di conversione è il seguente (assunto che dobbiamo convertire due matrici double[][]):

Partiamo dalle matrici che abbiamo preventivamente definito direttamente:

...
double[][] xor_input =
{
     new double[] {0.0,0.0},
     new double[] {1.0,0.0},
     new double[] {0.0,1.0},
     new double[] {1.0,1.0}
};
double[][] xor_idOutput =
{
    new double[] {0.0},
    new double[] {1.0},
    new double[] {1.0},
    new double[] {0.0}
};
...

Dichiariamo un array dello stesso numero di righe della prima matrice, questo sarà naturalmente un array di puntatori ad array (in C# è un array di tipo IntPtr).
Subito dopo, allochiamo per ognuno di questi puntatori lo spazio necessario per l’array. Infine, vi copiamo i dati dagli array gestiti.
Tale processo corrisponde, alla costruzione di una matrice in linguaggio di livello più basso.

...
IntPtr[] p2dArray = new IntPtr[4];

for (uint i = 0; i < rows; i++)
{
    p2dArray[i] = Marshal.AllocCoTaskMem(Marshal.SizeOf(xor_input[i][0])*2);
    Marshal.Copy(xor_input[i], 0, p2dArray[i], 2);
}
...

Fatto questo, allochiamo la memoria necessaria a contenere tutta l’intera matrice, poi copiamo.

...
IntPtr BufferIn = Marshal.AllocCoTaskMem(Marshal.SizeOf(p2dArray[0]) * 4);
Marshal.Copy(p2dArray, 0, BufferIn, 4);
...

Stesso procedimento vale anche per la seconda matrice:

...
for (uint i = 0; i < 4; i++)
{
    p2dArray[i] = Marshal.AllocCoTaskMem(Marshal.SizeOf(xor_idOutput[i][0]) * 1);
    Marshal.Copy(xor_idOutput[i], 0, p2dArray[i], 1);
}
IntPtr BufferOut = Marshal.AllocCoTaskMem(Marshal.SizeOf(p2dArray[0]) * 4);
Marshal.Copy(p2dArray, 0, BufferOut, 4);
...

Al termine può essere chiamata la funzione!

...
LibInvoke.Training(network, BufferIn, BufferOut, 4);
...

Il debug di una applicazione contenete sia codice C# che C++ può essere fatto abilitando nelle proprietà del progetto “debug di codice nativo“.


Archiviato in:C#, Informatica, Programmazione Tagged: Interoperability C#