Suggerimenti per l’utilizzo di una libreria C da C #

Ho una libreria in C che mi piacerebbe usare da C #.

Da quello che ho trovato su internet, un’idea è quella di avvolgerlo in una DLL C ++, e DllImport quello.

Il problema è che la funzione che voglio chiamare ha un set di parametri abbastanza brutto: include un riferimento a una funzione (che sarà una funzione .NET) e un paio di array (alcuni scrivono, altri leggono).

int lmdif(int (*fcn)(int, int, double*, double*, int*), int m, int n, double* x, double ftol, double xtol, double gtol, int maxfev, double epsfcn, double factor) 

Data un’interfaccia come questa, quali sono i nasties che dovrei cercare? (E anche soluzioni, per favore)

Perché non …

– Riscrivi in ​​C #? Ho iniziato, ma è già tradotto da FORTRAN e non mi piace molto scrivere codice che non riesco a capire.

– Usa una libreria .NET esistente ? Ci sto provando proprio ora, ma i risultati non sono esattamente gli stessi

– Ricompila in C ++? Ci sto pensando, ma sembra molto dolore

L’unico parametro “antipatico” è il puntatore della funzione. Ma per fortuna .NET li gestisce abbastanza bene tramite i delegati.

L’unico problema è con la convenzione di chiamata. In C #, emette solo un tipo (iirc stdcall ), mentre il codice C potrebbe aspettarsi cdecl . Quest’ultimo problema può essere comunque gestito a livello di IL (o usando Reflection.Emit ).

Ecco un codice che lo fa tramite Reflection.Emit (questo per aiutare a capire quale attributo psuedo deve essere inserito nel metodo Invoke del delegato).

Documentazione Microsoft per Marshal.GetFunctionPointerForDelegate qui :

“Converte un delegato in un puntatore a funzione richiamabile dal codice non gestito.”

Questo è il modo in cui generalmente interagisco con le DLL C da C #:

  public static unsafe class WrapCDll { private const string DllName = "c_functions.dll"; [DllImport(DllName, CallingConvention = CallingConvention.Cdecl)] public static extern void do_stuff_in_c(byte* arg); } 

Non è necessario eseguire il wrapping in C ++ e si ottiene la convenzione di chiamata necessaria.

Per la cronaca, ho ottenuto il massimo dal modo di farlo funzionare, poi ho finito per tirare i file C appropriati nel progetto C ++ (perché stavo combattendo con problemi di compatibilità nel codice che non avevo nemmeno bisogno).

Ecco alcuni suggerimenti che ho raccolto lungo il percorso che potrebbero essere utili per le persone che si verificano in questa domanda:

Matrici di marshalling

In C, non c’è differenza tra un puntatore a doppio ( double* ) e una matrice di doppi ( double* ). Quando vieni in attesa, devi essere in grado di disambiguare. Avevo bisogno di passare array di double , quindi la firma potrebbe apparire come questa:

 [DllImport(@"PathToDll")] public static extern Foo( [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)[In] double[] x, [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 3)[Out] double[] y, int x_size, int y_size) 

C ha bisogno di ulteriori informazioni sulla durata dell’array, che devi passare come parametro separato. Anche il marshalling deve sapere dove si trova questa dimensione, quindi si specifica SizeParamIndex , che indica l’indice a base zero del parametro size nell’elenco dei parametri.

Si specifica inoltre in quale direzione deve essere passato l’array. In questo esempio, x è passato in Foo , che ‘rimanda’ y .

Convocazione

In realtà, non è necessario comprendere i dettagli più fini di ciò che significa (in altre parole, io no), è sufficiente sapere che esistono convenzioni di chiamata diverse e che devono corrispondere su entrambi i lati. Il C # predefinito è StdCall , il C predefinito è Cdecl . Ciò significa che è necessario specificare esplicitamente la convenzione di chiamata solo se è diversa dall’impostazione predefinita su cui si sta utilizzando.

Questo è particolarmente peloso nel caso di una richiamata. Se passiamo una callback a C da C# , intendiamo richiamare quella richiamata con StdCall , ma quando la passiamo, stiamo usando Cdecl . Ciò comporta le seguenti firme (vedi questa domanda per il contesto):

 //=======C-code====== //type signature of callback function typedef int (__stdcall *FuncCallBack)(int, int); void SetWrappedCallback(FuncCallBack); //here default = __cdecl //======C# code====== public delegate int FuncCallBack(int a, int b); // here default = StdCall [DllImport(@"PathToDll", CallingConvention = CallingConvention.Cdecl)] private static extern void SetWrappedCallback(FuncCallBack func); 

Imballaggio della richiamata

Ovvio, ma non era immediatamente ovvio per me:

 int MyFunc(int a, int b) { return a * b; } //... FuncCallBack ptr = new FuncCallBack(MyFunc); SetWrappedCallback(ptr); 

.def file

Qualsiasi funzione che si desidera esporre dal progetto C ++ (che deve essere DllImport ed), deve ModDef.def file ModDef.def , il cui contenuto sarà simile a questo:

 LIBRARY MyLibName EXPORTS SetWrappedCallback @1 

extern “C”

Se si desidera utilizzare le funzioni C da C ++, è necessario dichiararle come extern "C" . Se includi un file di intestazione delle funzioni C, procedi in questo modo:

 extern "C" { #include "C_declarations.h" } 

Intestazioni precompilate

Un’altra cosa che dovevo fare per evitare errori di compilazione era Right-click -> Properties -> C/C++ -> Precompiled Headers e impostare Precompiled header su Non utilizzare intestazioni precompilate per ogni file ‘C’.

C’è stato un articolo su MSDN anni fa che ha avuto successo in InteropSignatureToolkit . Questo piccolo strumento è ancora utile per interfacciare le interfacce C. Copia e incolla il codice dell’interfaccia nel “SigImp Translation Sniplet” e osserva i risultati.

Il risultato è il seguente ma non ho idea di come viene utilizzato il delegato o se funziona. Quindi se funziona aggiungi alcuni commenti.

 /// Return Type: int ///param0: int ///param1: int ///param2: double* ///param3: double* ///param4: int* public delegate int Anonymous_83fd32fd_91ee_45ce_b7e9_b7d886d2eb38(int param0, int param1, ref double param2, ref double param3, ref int param4); public partial class NativeMethods { /// Return Type: int ///fcn: Anonymous_83fd32fd_91ee_45ce_b7e9_b7d886d2eb38 ///m: int ///n: int ///x: double* ///ftol: double ///xtol: double ///gtol: double ///maxfev: int ///epsfcn: double ///factor: double [System.Runtime.InteropServices.DllImportAttribute("", EntryPoint="lmdif", CallingConvention=System.Runtime.InteropServices.CallingConvention.Cdecl)] public static extern int lmdif(Anonymous_83fd32fd_91ee_45ce_b7e9_b7d886d2eb38 fcn, int m, int n, ref double x, double ftol, double xtol, double gtol, int maxfev, double epsfcn, double factor) ; } 

Ecco un modo per inviare MOLTI dati numerici alla tua funzione C tramite un StringBuilder. È sufficiente scaricare i numeri in un object StringBuilder, mentre si impostano i delimitatori nelle posizioni appropriate e voilá:

 class Program { [DllImport("namEm.DLL", CallingConvention = CallingConvention.Cdecl, EntryPoint = "nameEm", CharSet = CharSet.Ansi, BestFitMapping = false, ThrowOnUnmappableChar = true)] public static extern int nameEm([MarshalAs(UnmanagedType.LPStr)] StringBuilder str); static void Main(string[] args) { int m = 3; StringBuilder str = new StringBuilder(); str.Append(String.Format("{0};", m)); str.Append(String.Format("{0} {1:E4};", 5, 76.334E-3 )); str.Append(String.Format("{0} {1} {2} {3};", 65,45,23,12)); m = nameEm(str); } } 

E sul lato C, prendi lo StringBuilder come carattere *:

 extern "C" { __declspec(dllexport) int __cdecl nameEm(char* names) { int n1, n2, n3[4]; char *token, *next_token2 = NULL, *next_token = NULL; float f; sscanf_s(strtok_s(names, ";", &next_token), "%d", &n2); sscanf_s(strtok_s(NULL, ";", &next_token), "%d %f", &n1, &f); // Observe the use of two different strtok-delimiters. // the ';' to pick the sequence of 4 integers, // and the space to split that same sequence into 4 integers. token = strtok_s(strtok_s(NULL, ";", &next_token)," ",&next_token2); for (int i=0; i < 4; i++) { sscanf_s(token,"%d", &n3[i]); token = strtok_s(NULL, " ",&next_token2); } return 0; } }