2009-12-15 11 views
9

Probabilmente una domanda noob ma l'interoperabilità non è ancora uno dei miei punti di forza..NET Interop IntPtr vs. ref

A parte la limitazione del numero di sovraccarichi v'è alcuna ragione per cui dovrei dichiarare i miei DllImports come:

[DllImport("user32.dll")] 
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam); 

e usarli come questo:

IntPtr lParam = Marshal.AllocCoTaskMem(Marshal.SizeOf(formatrange)); 
Marshal.StructureToPtr(formatrange, lParam, false); 

int returnValue = User32.SendMessage(_RichTextBox.Handle, ApiConstants.EM_FORMATRANGE, wParam, lParam); 

Marshal.FreeCoTaskMem(lParam); 

Piuttosto che creare un sovraccarico mirato:

[DllImport("user32.dll")] 
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref FORMATRANGE lParam); 

E come utilizzare:

FORMATRANGE lParam = new FORMATRANGE(); 
int returnValue = User32.SendMessage(_RichTextBox.Handle, ApiConstants.EM_FORMATRANGE, wParam, ref lParam); 

Il sovraccarico di ref finisce per essere più facile da usare ma mi chiedo se c'è un inconveniente di cui non sono a conoscenza.

Edit:

Un sacco di informazioni grande ragazzi finora.

@P Daddy: hai un esempio di basare la classe struct su una classe astratta (o qualsiasi)? Ho cambiato la mia firma a:

[DllImport("user32.dll", SetLastError = true)] 
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, [In, Out, MarshalAs(UnmanagedType.LPStruct)] CHARFORMAT2 lParam); 

Senza la In, Out, e MarshalAs il SendMessage (EM_GETCHARFORMAT nel mio test) fallire. L'esempio di cui sopra funziona bene, ma se cambio a:

[DllImport("user32.dll", SetLastError = true)] 
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, [In, Out, MarshalAs(UnmanagedType.LPStruct)] NativeStruct lParam); 

ho uno System.TypeLoadException che dice il formato CHARFORMAT2 non è valido (cercherò e la cattura per qui).

L'eccezione:

Impossibile caricare il tipo 'CC.Utilities.WindowsApi.CHARFORMAT2' da assemblaggio 'CC.Utilities, Version = 1.0.9.1212, Culture = neutral, PublicKeyToken = 111aac7a42f7965e' perché il formato è valido .

La classe NativeStruct:

public class NativeStruct 
{ 
} 

ho provato abstract, aggiungendo l'attributo StructLayout, ecc e ho la stessa eccezione.

[StructLayout(LayoutKind.Sequential)] 
public class CHARFORMAT2: NativeStruct 
{ 
    ... 
} 

Edit:

non ho seguito le FAQ e ho fatto una domanda che può essere discusso, ma non ha risposto positivamente. A parte questo ci sono state molte informazioni penetranti in questa discussione. Quindi lascio ai lettori la possibilità di votare una risposta. Il primo a più di 10 voti positivi sarà la risposta.Se nessuna risposta soddisfa questa in due giorni (12/17 PST) io aggiungo la mia risposta personale che riassume tutte le conoscenze gustoso nel thread :-)

Edit Anche in questo caso:

ho mentito, accettando P La risposta di papà perché lui è l'uomo ed è stato di grande aiuto (ha anche una piccola scimmia carina :-P)

+1

La classe NativeStruct richiede anche l'attributo 'StructLayout'. Pubblicherò il mio codice di esempio funzionante. –

risposta

15

Se la struct è marshalable senza elaborazione personalizzata, preferisco di gran lunga il secondo approccio, in cui dichiari la funzione p/invoke come prendendo un ref (puntatore a) del tuo tipo. In alternativa, puoi dichiarare i tuoi tipi come classi anziché come strutture, quindi puoi passare anche a null.

[StructLayout(LayoutKind.Sequential)] 
struct NativeType{ 
    ... 
} 

[DllImport("...")] 
static extern bool NativeFunction(ref NativeType foo); 

// can't pass null to NativeFunction 
// unless you also include an overload that takes IntPtr 

[DllImport("...")] 
static extern bool NativeFunction(IntPtr foo); 

// but declaring NativeType as a class works, too 

[StructLayout(LayoutKind.Sequential)] 
class NativeType2{ 
    ... 
} 

[DllImport("...")] 
static extern bool NativeFunction(NativeType2 foo); 

// and now you can pass null 

<pedantry>

Tra l'altro, nel tuo esempio passando un puntatore come IntPtr, hai utilizzato il torto Alloc. SendMessage non è una funzione COM, quindi non si dovrebbe utilizzare l'allocatore COM. Utilizzare Marshal.AllocHGlobal e Marshal.FreeHGlobal. Sono scarsamente nominati; i nomi hanno senso solo se hai fatto la programmazione dell'API di Windows, e forse neanche allora. AllocHGlobal chiama GlobalAlloc in kernel32.dll, che restituisce un HGLOBAL. Questo utilizzato da è diverso da uno HLOCAL, restituito da LocalAlloc indietro nei giorni di 16 bit, ma in Windows a 32 bit sono gli stessi.

L'uso del termine HGLOBAL per riferirsi a un blocco di memoria (nativo) user-space solo tipo di bloccato, immagino, e le persone che progettano la classe Marshal non devono aver avuto il tempo di pensare a come poco intuitivo che sarebbe per la maggior parte degli sviluppatori .NET. D'altra parte, la maggior parte degli sviluppatori .NET non hanno bisogno di allocare memoria non gestita, così ....

</pedantry>


Modifica

Lei parla che stai ricevendo un TypeLoadException quando si utilizza una classe anziché una struct e si richiede un campione. Ho fatto un rapido test usando CHARFORMAT2, poiché sembra che sia quello che stai cercando di usare.

First ABC :

[StructLayout(LayoutKind.Sequential)] 
abstract class NativeStruct{} // simple enough 

è richiesto l'attributo StructLayout, oppure si sarà ottenere un TypeLoadException.

Ora la CHARFORMAT2 classe:

[StructLayout(LayoutKind.Sequential, Pack=4, CharSet=CharSet.Auto)] 
class CHARFORMAT2 : NativeStruct{ 
    public DWORD cbSize = (DWORD)Marshal.SizeOf(typeof(CHARFORMAT2)); 
    public CFM  dwMask; 
    public CFE  dwEffects; 
    public int  yHeight; 
    public int  yOffset; 
    public COLORREF crTextColor; 
    public byte  bCharSet; 
    public byte  bPitchAndFamily; 
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=32)] 
    public string szFaceName; 
    public WORD  wWeight; 
    public short sSpacing; 
    public COLORREF crBackColor; 
    public LCID  lcid; 
    public DWORD dwReserved; 
    public short sStyle; 
    public WORD  wKerning; 
    public byte  bUnderlineType; 
    public byte  bAnimation; 
    public byte  bRevAuthor; 
    public byte  bReserved1; 
} 

ho usato using dichiarazioni di alias System.UInt32 come DWORD, LCID e COLORREF, e l'alias System.UInt16 come WORD. Cerco di mantenere le mie definizioni P/Invoke fedele alle specifiche SDK come posso. CFM e CFE sono enums che contengono i valori di flag per questi campi. Ho lasciato le loro definizioni per brevità, ma posso aggiungerle se necessario.

ho dichiarato SendMessage come:

[DllImport("user32.dll", CharSet=CharSet.Auto)] 
static extern IntPtr SendMessage(
    HWND hWnd, MSG msg, WPARAM wParam, [In, Out] NativeStruct lParam); 

HWND è un alias per System.IntPtr, MSG è System.UInt32, e WPARAM è System.UIntPtr.

l'attributo [In, Out] su lParam è necessario per questo funzionamento, in caso contrario, non sembra avere il marshalling in entrambe le direzioni (prima e dopo la chiamata al codice nativo).

mi chiamano con:

CHARFORMAT2 cf = new CHARFORMAT2(); 
SendMessage(rtfControl.Handle, (MSG)EM.GETCHARFORMAT, (WPARAM)SCF.DEFAULT, cf); 

EM e SCF sono enum s che ho, ancora una volta, lasciato fuori per (relativa) brevità.

verifico con successo:

Console.WriteLine(cf.szFaceName); 

e ottengo:

Microsoft Sans Serif

funziona come un fascino!


Um, o meno, a seconda di quanto sonno hai avuto e quante cose si sta cercando di fare in una sola volta, suppongo.

Questo funzionerebbe se CHARFORMAT2 fosse un tipo blittable. (Un tipo blittable è un tipo che ha la stessa rappresentazione nella memoria gestita come nella memoria non gestita.) Ad esempio, il tipo MINMAXINFOfa come descritto.

[StructLayout(LayoutKind.Sequential)] 
class MINMAXINFO : NativeStruct{ 
    public Point ptReserved; 
    public Point ptMaxSize; 
    public Point ptMaxPosition; 
    public Point ptMinTrackSize; 
    public Point ptMaxTrackSize; 
} 

Ciò è dovuto al fatto che i tipi blittable non vengono realmente sottoposti a marshalling. Sono solo bloccati in memoria, questo impedisce al GC di spostarli e l'indirizzo della loro posizione nella memoria gestita viene passato alla funzione nativa.

È necessario eseguire il marshalling dei tipi non blittibili. Il CLR assegna la memoria non gestita e copia i dati tra l'oggetto gestito e la sua rappresentazione non gestita, apportando le necessarie conversioni tra i formati man mano che procede.

La struttura CHARFORMAT2 non è blitta a causa del membro string. Il CLR non può semplicemente passare un puntatore a un oggetto .NET string in cui è previsto un array di caratteri a lunghezza fissa. Quindi la struttura CHARFORMAT2 deve essere eseguita il marshalling.

Come sembrerebbe, per eseguire correttamente il marshalling, la funzione di interoperabilità deve essere dichiarata con il tipo da eseguire il marshalling. In altre parole, data la definizione di cui sopra, il CLR deve effettuare una sorta di determinazione basata sul tipo statico di NativeStruct. Direi che sta rilevando correttamente che l'oggetto deve essere eseguito il marshalling, ma solo "esegue il marshalling" di un oggetto a byte zero, la dimensione di NativeStruct stesso.

Così, al fine di ottenere il vostro codice di lavoro per CHARFORMAT2 (e qualsiasi altro tipo non copiabili si potrebbe utilizzare), si dovrà tornare a dichiarare SendMessage come prendere un oggetto CHARFORMAT2. Mi dispiace ti ho portato fuori strada su questo.


Captcha per la modifica precedente:

il whippet

Sì, frusta bene!


Cory,

Questo è fuori tema, ma ho notato un potenziale problema per voi in app sembra che si sta facendo.

Il controllo rich textbox utilizza le funzioni standard di misurazione del testo e di disegno del testo GDI. Perché questo è un problema? Perché, nonostante affermi che un carattere TrueType è uguale sullo schermo come su carta, GDI non posiziona correttamente i caratteri. Il problema è l'arrotondamento.

GDI utilizza routine all-inte per misurare il testo e posizionare caratteri. La larghezza di ciascun carattere (e l'altezza di ogni riga, per quella materia) è arrotondata al numero intero più vicino di pixel, senza correzione degli errori.

L'errore può essere facilmente visualizzato nell'app di prova. Imposta il carattere su Courier New a 12 punti.Questo carattere a larghezza fissa deve contenere caratteri esattamente 10 per pollice o 0,1 pollici per carattere. Questo dovrebbe significare che, data la larghezza della tua linea di partenza di 5,5 pollici, dovresti essere in grado di inserire 55 caratteri sulla prima riga prima che avvenga l'avvolgimento.

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123

Ma se ci provate, vedrete che il ritorno si verifica dopo soli 54 caratteri. Inoltre, il numero 54 th e una parte del numero 53 rd sporgono dal margine apparente mostrato sulla barra del righello.

Questo presuppone che le impostazioni siano a 96 DPI standard (tipi di carattere normali). Se utilizzi 120 DPI (caratteri grandi), non visualizzerai questo problema, anche se in questo caso sembra che il controllo della dimensione non sia corretto. Probabilmente non lo vedrai sulla pagina stampata.

Cosa sta succedendo qui? Il problema è che 0.1 pollici (la larghezza di un carattere) è 9.6 pixel (di nuovo, usando 96 DPI). GDI non spazio caratteri utilizzando numeri in virgola mobile, quindi arrotonda fino a 10 pixel. Quindi 55 caratteri occupano 55 * 10 = 550 pixel/96 DPI = 5.7291666 ... pollici, mentre quello che ci aspettavamo era 5,5 pollici.

Mentre questo sarà probabilmente meno evidente nel caso di utilizzo normale per un programma di elaborazione testi, esiste una probabilità di casi in cui il ritorno a capo avviene in diversi punti sullo schermo rispetto a pagina, o che le cose non allineano il stesso una volta stampato come hanno fatto sullo schermo. Questo potrebbe rivelarsi un problema per te se questa è un'applicazione commerciale su cui stai lavorando.

Sfortunatamente, la soluzione per questo problema non è semplice. Significa che dovrai rinunciare al ricco controllo della casella di testo, il che significa un enorme problema di implementare te stesso tutto ciò che fa per te, il che è molto. Significa anche che il codice di disegno del testo che dovrai implementare diventa piuttosto complicato. Ho il codice che lo fa, ma è troppo complesso per postare qui. Potrebbe, tuttavia, trovare this example o this one utile.

Buona fortuna!


classe astratta di base

+0

+1 per la struct -> promemoria della classe. Se lo facessi potrei creare un costruttore vuoto per le mie strutture come CHARFORMAT che hanno una proprietà cbSize per versioni diverse. Vorrei salvarmi dal dover Marshal.SizeOf() ogni volta. –

+1

Può anche evitare di avere molti sovraccarichi su 'SendMessage'. È possibile derivare tutte le definizioni del tipo nativo da una singola classe astratta e dichiarare 'SendMessage' per prendere quella classe. –

+0

Grazie per tutte le fantastiche informazioni finora. Non avevo idea della differenza HGLOBAL/HLOCAL. Il mio esempio era qualcosa che avevo trovato su internet ma stavo andando sulla rotta 'ref'.In ogni caso sono molto interessato alla tua soluzione per utilizzare una classe base astratta ma sto incontrando alcuni problemi (vedi la mia modifica). Continuerò a cercare di capirlo. –

1

Non vedo alcun inconveniente.

By-ref è spesso sufficiente per tipo semplice e struttura semplice.

IntPtr deve essere preferito se la struttura ha una dimensione variabile o se si desidera eseguire un'elaborazione personalizzata.

3

Ho avuto alcuni casi divertenti in cui un parametro è qualcosa di simile ref Guid parent e la relativa documentazione dice:.

"puntatore a un GUID che specifica il genitore passare un puntatore nullo per usare [inserire qualche elemento definito dal sistema] . "

Se null (o IntPtr.Zero per IntPtr parametri) è davvero un parametro non valido, allora stai bene utilizzando un parametro ref - forse anche meglio visto che è più chiaro esattamente ciò che serve per passare.

Se null è un parametro valido, è possibile passare ClassType anziché ref StructType. Gli oggetti di un tipo di riferimento (class) vengono passati come puntatore e consentono null.

+0

Buon punto ... +1 –

1

Utilizzo di ref L'utilizzo di ref è più semplice e meno soggetto a errori rispetto alla manipolazione manuale dei puntatori, quindi non vedo alcun motivo per non utilizzarlo ... Un altro vantaggio dell'utilizzo di ref è che non è necessario liberare memoria allocata non gestita

2

No, non è possibile sovraccaricare SendMessage e rendere l'argomento wParam un int. Ciò renderà il tuo programma fallito su una versione a 64 bit del sistema operativo. Deve essere un puntatore, IntPtr, un riferimento separabile o un tipo di valore out o ref. Sovraccarico del tipo di uscita/ref è altrimenti bene.


EDIT: Come ha sottolineato l'OP, questo non è effettivamente un problema. La convenzione di chiamata della funzione a 64 bit passa i primi 4 argomenti attraverso i registri, non lo stack. Non vi è quindi alcun pericolo di disallineamento dello stack per gli argomenti wparam e lparam.

+0

Hai qualche esempio in cui wParam è specificato come puntatore? Certo, sto solo iniziando a scavare in questo, ma molti di quelli che ho visto non sono stati dei puntatori per il wParam, come http://msdn.microsoft.com/en-us/library/bb788026(VS.85).aspx per esempio. O forse dovrebbero essere dei puntatori ma la descrizione non lo specifica come fa per lParam. –

+0

Indipendentemente dal fatto che sia usato come puntatore, la funzione prevede 8 byte da passare in x64. Passerai 4, disallineare gli altri argomenti. –

+0

La ragione per cui mi sto interrogando è perché se uso Reflector per guardare System.Windows.Forms.UnsafeNativeMethods vedo molti overload di SendMessage. La maggior parte di loro prende un Int32 per wParam. Certo, ce ne sono alcuni che prendono IntPtr anche se in uso (dalla mia navigazione casuale) sono preferiti i sovraccarichi wParam di Int32. Quindi ovviamente c'è un motivo per usare un puntatore, ma mi chiedo quale sia la ragione. Ovviamente se il messaggio dice di usare un punto allora ... ma se dice "wParam - questo parametro non è usato, deve essere zero". o "wParam - Uno dei seguenti (const definito da uint)" –