2015-08-20 10 views
6

ScenarioC# Marshalling bool

Questo dovrebbe essere un compito facile, ma per qualche motivo non riesco a farlo andare come previsto. Devo eseguire il marshalling di base su C++ struct durante una chiamata P/Invoke inversa (codice gestito chiamata non gestita).

Il problema si pone solo quando si utilizza bool all'interno della struct, quindi ho solo rivestimento laterale C++ fino a:

struct Foo { 
    bool b; 
}; 

Dal marescialli .NET booleani come campi a 4 byte per impostazione predefinita, ho maresciallo il booleano nativo esplicitamente come campo 1 byte di lunghezza:

public struct Foo { 
    [MarshalAs(UnmanagedType.I1)] public bool b; 
} 

Quando chiamo un metodo statico gestito esportato con la seguente firma e del corpo:

public static void Bar(Foo foo) { 
    Console.WriteLine("{0}", foo.b); 
} 

Ottengo la corretta rappresentazione alfa booleana stampata. Se estendo la struttura con più campi, l'allineamento è corretto e i dati non sono corrotti dopo il marshalling.

Problema

Per qualche ragione, se non supero questo Marshalled struct come argomento, ma piuttosto come un tipo di ritorno per valore:

public static Foo Bar() { 
    var foo = new Foo { b = true }; 
    return foo; 
} 

l'applicazione si blocca con il seguente messaggio di errore :

enter image description here

Se io modificare la struttura riuscito a tenere un byte invece di un valore bool

public struct Foo { 
    [MarshalAs(UnmanagedType.I1)] public byte b; 
} 

public static Foo Bar() { 
    var foo = new Foo { b = 1 }; 
    return foo; 
} 

alla dichiarazione schierato correttamente senza un errore da un bool non gestito.

non lo faccio Unterstand due cose qui:

  1. Perché un paramter schierò con bool come descritto in precedenza il lavoro, ma come valore di ritorno dà un errore?
  2. Perché uno eseguito come UnmanagedType.I1 funziona come restituisce, ma un bool ha anche il marshalling con UnmanagedType.I1 no?

Spero che la mia descrizione abbia un senso - in caso contrario, fatemelo sapere, così posso cambiare il testo.

EDIT: mia soluzione attuale è una struct gestito come:

public struct Foo { 
    private byte b; 
    public bool B { 
     get { return b != 0; } 
     set { b = value ? (byte)1 : (byte)0; } 
} 

che onestamente, trovo abbastanza ridicolo ...

EDIT2: Ecco un quasi-MCVE.L'assembly gestito è stato ricompilato con le esportazioni di simboli corrette (utilizzando gli attributi e .vtentry nel codice IL), ma lo dovrebbe essere non fare alcuna differenza per le chiamate C++/CLI. Quindi, questo codice non funziona "così com'è", senza fare le esportazioni manualmente:

C++ (Native.dll):

#include <Windows.h> 

struct Foo { 
    bool b; 
}; 

typedef void (__stdcall *Pt2PassFoo)(Foo foo); 
typedef Foo (__stdcall *Pt2GetFoo)(void); 

int main(int argc, char** argv) { 
    HMODULE mod = LoadLibraryA("managed.dll"); 
    Pt2PassFoo passFoo = (Pt2PassFoo)GetProcAddress(mod, "PassFoo"); 
    Pt2GetFoo getFoo = (Pt2GetFoo)GetProcAddress(mod, "GetFoo"); 

    // Try to pass foo (THIS WORKS) 
    Foo f1; 
    f1.b = true; 
    passFoo(f1); 

    // Try to get foo (THIS FAILS WITH ERROR ABOVE) 
    // Note that the managed method is indeed called; the error 
    // occurs upon return. If 'b' is not a 'bool' but an 'int' 
    // it also works, so there must be something wrong with it 
    // being 'bool'. 
    Foo f2 = getFoo(); 

    return 0; 
} 

C# (managed.dll):

using System; 
using System.Runtime.InteropServices; 

public struct Foo { 
    [MarshalAs(UnmanagedType.I1)] public bool b; 
    // When changing the above line to this, everything works fine! 
    // public byte b; 
} 

/* 
    .vtfixup [1] int32 fromunmanaged at VT_01 
    .vtfixup [1] int32 fromunmanaged at VT_02 
    .data VT_01 = int32(0) 
    .data VT_02 = int32(0) 
*/ 

public static class ExportedFunctions { 
    public static void PassFoo(Foo foo) { 
     /* 
      .vtentry 1:1 
      .export [1] as PassFoo 
     */    

     // This prints the correct value, and the 
     // method returns without error. 
     Console.WriteLine(foo.b); 
    } 

    public static Foo GetFoo() { 
     /* 
      .vtentry 2:1 
      .export [2] as GetFoo 
     */ 

     // The application crashes with the shown error 
     // message upon return. 
     var foo = new Foo { b = true; } 
     return foo; 
    } 
} 
+0

Possiamo avere un MCVE? Quindi possiamo vedere anche le chiamate di funzione. –

+0

@DavidHeffernan Ho aggiunto un MCVE come meglio potevo. Poiché utilizzo meccanismi di inversione P/Invoke e manipolo direttamente il codice IL, esso non funzionerà out-of-the-box senza modifiche manuali all'assieme. – PuerNoctis

+0

Manipola l'IL? Hmm. Sicuramente sarà rilevante !! –

risposta

5

Il problema di fondo è lo stesso di questa domanda - Why DllImport for C bool as UnmanagedType.I1 throws but as byte it works L'eccezione che si ottiene è MarshalDirectiveException - ottenere le restanti informazioni sull'eccezione è un po 'ingannevole r, ma non necessario.

In breve, il marshalling per i valori di ritorno funziona solo per strutture blitteble. Quando si specifica di utilizzare un campo booleano, la struttura non è più conciliabile (poiché bool non è modificabile) e non funzionerà più per i valori restituiti. Questa è semplicemente una limitazione del marshaller e vale sia per DllImport sia per i tentativi di "DllExport".

Citando il pezzo rilevante della documentazione:

strutture che vengono restituiti dalla piattaforma invocare le chiamate devono essere di tipo copiabili. Il richiamo della piattaforma non supporta le strutture non blitabili come tipi di ritorno.

Non è detto a titolo definitivo, ma la stessa cosa si applica quando viene invocato.

La soluzione più semplice è quella di attenersi al proprio approccio "byte come backing, bool as property". In alternativa, è possibile utilizzare il C BOOL, che funzionerà perfettamente. E, naturalmente, c'è sempre la possibilità di usare un wrapper C++/CLI, o anche solo nascondere il reale layout della struttura nei metodi di supporto (in questo caso, i metodi di esportazione chiameranno un altro metodo che si occupa del tipo reale e gestire la corretta conversione al tipo Foo++).

È anche possibile utilizzare un argomento ref anziché un valore restituito. Questo è in realtà un modello comune di interoperabilità non gestito:

typedef void(__stdcall *Pt2GetFoo)(Foo* foo); 

Foo f2 = Foo(); 
getFoo(&f2); 

sul lato C++, e

public static void GetFoo(ref Foo foo) 
{ 
    foo = new Foo { b = true }; 
} 

sul lato C#.

Si potrebbe anche fare il vostro proprio tipo booleano, una semplice struct con un unico byte campo, con gli operatori del cast impliciti da e per bool - non sta andando a lavorare esattamente come un vero e proprio campo bool, ma dovrebbe funzionare bene più del tempo.

+0

Hmm, non hai letto correttamente quel duplicato. L'uso di BOOL non è una soluzione alternativa, non è conciliabile. Non può essere, un bool .NET è 1 byte e BOOL è 4 byte. –

+0

@ HansPassant Oh, mio ​​male. Fisso. – Luaan