c# - .NET Interop IntPtr vs. ref
winapi (5)
No veo inconvenientes.
Por ref es a menudo suficiente para el tipo simple y la estructura simple.
IntPtr debe ser favorecido si la estructura tiene un tamaño variable o si desea hacer un procesamiento personalizado.
Probablemente sea una pregunta novato, pero la interoperabilidad no es uno de mis puntos fuertes todavía.
Además de limitar el número de sobrecargas, hay alguna razón por la que debería declarar mis DllImports como:
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, IntPtr lParam);
Y úsalos así:
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);
En lugar de crear una sobrecarga específica:
[DllImport("user32.dll")]
public static extern int SendMessage(IntPtr hWnd, int msg, int wParam, ref FORMATRANGE lParam);
Y usándolo como:
FORMATRANGE lParam = new FORMATRANGE();
int returnValue = User32.SendMessage(_RichTextBox.Handle, ApiConstants.EM_FORMATRANGE, wParam, ref lParam);
La sobrecarga por ref resulta ser más fácil de usar, pero me pregunto si hay un inconveniente del que no tengo conocimiento.
Editar:
Mucha mucha información hasta ahora chicos.
@P Daddy: ¿Tiene un ejemplo de basar la clase struct de una clase abstracta (o cualquiera)? Cambié mi 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);
Sin In
, Out
y MarshalAs
al MarshalAs
SendMessage (EM_GETCHARFORMAT en mi prueba) falla. El ejemplo anterior funciona bien, pero si lo 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);
Obtengo una System.TypeLoadException que dice que el formato CHARFORMAT2 no es válido (intentaré capturarlo aquí).
La excepción:
No se pudo cargar el tipo ''CC.Utilities.WindowsApi.CHARFORMAT2'' del ensamblado ''CC.Utilities, Version = 1.0.9.1212, Culture = neutral, PublicKeyToken = 111aac7a42f7965e'' porque el formato no es válido.
La clase NativeStruct:
public class NativeStruct
{
}
Intenté abstract
, StructLayout
atributo StructLayout
, etc. y obtuve la misma excepción.
[StructLayout(LayoutKind.Sequential)]
public class CHARFORMAT2: NativeStruct
{
...
}
Editar:
No seguí las preguntas frecuentes y formulé una pregunta que puede ser discutida pero no respondida positivamente. Aparte de eso, ha habido mucha información perspicaz en este hilo. Así que dejaré que los lectores voten por una respuesta. La respuesta será de 1 a más de 10 votos. Si ninguna respuesta cumple esto en dos días (12/17 PST), agregaré mi propia respuesta que resume todos los sabrosos conocimientos en el hilo :-)
Editar nuevamente:
Mentí, aceptando la respuesta de P Daddy porque él es el hombre y me ha sido de gran ayuda (también tiene un lindo mono pequeño :-P)
Usar ref
es más simple y menos propenso a errores que manipular punteros manualmente, así que no veo una buena razón para no usarlo ... Otro beneficio de usar ref
es que no tiene que preocuparse por liberar memoria asignada no administrada
No, no puede sobrecargar SendMessage y hacer que el argumento wparam sea int. Eso hará que su programa falle en una versión de 64 bits del sistema operativo. Tiene que ser un puntero, ya sea IntPtr, una referencia blittable o un tipo de valor de salida o ref. Sobrecargar el tipo de salida / ref es por lo demás correcto.
EDITAR: Como señaló el OP, esto no es realmente un problema. La convención de llamada de función de 64 bits pasa los primeros 4 argumentos a través de registros, no la pila. Por lo tanto, no hay peligro de que la pila se desalinee para los argumentos wparam y lparam.
Si la estructura es ordenable sin procesamiento personalizado, prefiero mucho el último enfoque, donde declaras que la función p / invoke toma un ref
(puntero) a tu tipo. Alternativamente, puede declarar sus tipos como clases en lugar de estructuras, y luego puede pasar null
, también.
[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>
Por cierto, en su ejemplo al pasar un puntero como
IntPtr
, ha utilizado elAlloc
incorrecto.SendMessage
no es una función COM, por lo que no debe utilizar el asignador COM. UsaMarshal.AllocHGlobal
yMarshal.FreeHGlobal
. Están mal nombrados; los nombres solo tienen sentido si has hecho la programación de la API de Windows, y tal vez ni siquiera entonces.AllocHGlobal
llama aGlobalAlloc
en kernel32.dll, que devuelve unHGLOBAL
. Esto solía ser diferente deHLOCAL
, devuelto porLocalAlloc
en los días de 16 bits, pero en Windows de 32 bits son los mismos.El uso del término
HGLOBAL
para referirse a un bloque de memoria de espacio de usuario (nativo) simplemente se atascó, supongo, y las personas que diseñaron la claseMarshal
no debieron haberse tomado el tiempo de pensar en lo poco intuitivo que sería para la mayoría Desarrolladores .NET. Por otro lado, la mayoría de los desarrolladores .NET no necesitan asignar memoria no administrada, así que ...
</pedantry>
Editar
Menciona que obtiene una TypeLoadException cuando usa una clase en lugar de una estructura, y solicita una muestra. Hice una prueba rápida usando CHARFORMAT2
, ya que parece que eso es lo que estás tratando de usar.
Primero el ABC 1 :
[StructLayout(LayoutKind.Sequential)]
abstract class NativeStruct{} // simple enough
Se requiere el atributo StructLayout
, o obtendrá una TypeLoadException.
Ahora la clase CHARFORMAT2
:
[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;
}
He usado el using
sentencias para alias System.UInt32
como DWORD
, LCID
y COLORREF
, y alias System.UInt16
como WORD
. Intento mantener mis definiciones de P / Invoke como verdaderas a la especificación del SDK como puedo. CFM
y CFE
son enums
que contienen los valores de bandera para estos campos. Dejé sus definiciones por brevedad, pero puedo agregarlas si es necesario.
He declarado SendMessage
como:
[DllImport("user32.dll", CharSet=CharSet.Auto)]
static extern IntPtr SendMessage(
HWND hWnd, MSG msg, WPARAM wParam, [In, Out] NativeStruct lParam);
HWND
es un alias para System.IntPtr
, MSG
es System.UInt32
y WPARAM
es System.UIntPtr
.
[In, Out]
atributo [In, Out]
en lParam
es necesario para que esto funcione; de lo contrario, no parece que se haya clasificado en ambas direcciones (antes y después de la llamada al código nativo).
Lo llamo con:
CHARFORMAT2 cf = new CHARFORMAT2();
SendMessage(rtfControl.Handle, (MSG)EM.GETCHARFORMAT, (WPARAM)SCF.DEFAULT, cf);
EM
y SCF
son enum
s He, de nuevo, dejado de lado por (relativa) brevedad.
Verifico el éxito con:
Console.WriteLine(cf.szFaceName);
y obtengo:
Microsoft Sans Serif
¡Funciona de maravilla!
Um, o no, dependiendo de cuánto dormiste y cuántas cosas intentas hacer a la vez, supongo.
Esto funcionaría si CHARFORMAT2
fuera un tipo blittable . (Un tipo blittable es un tipo que tiene la misma representación en la memoria administrada que en la memoria no administrada). Por ejemplo, el tipo MINMAXINFO
funciona como se describe.
[StructLayout(LayoutKind.Sequential)]
class MINMAXINFO : NativeStruct{
public Point ptReserved;
public Point ptMaxSize;
public Point ptMaxPosition;
public Point ptMinTrackSize;
public Point ptMaxTrackSize;
}
Esto se debe a que los tipos blittables no están realmente calculados. Simplemente están fijados en la memoria, esto impide que el GC los mueva, y la dirección de su ubicación en la memoria administrada se transfiere a la función nativa.
Los tipos no blittable tienen que ser calculados. El CLR asigna memoria no administrada y copia los datos entre el objeto administrado y su representación no administrada, realizando las conversiones necesarias entre formatos a medida que avanza.
La estructura CHARFORMAT2
no es blittable debido al miembro de string
. El CLR no puede simplemente pasar un puntero a un objeto de string
.NET donde se espera que esté una matriz de caracteres de longitud fija. Entonces la estructura CHARFORMAT2
debe ser ordenada.
Como parece, para que se produzca la clasificación correcta, la función de interoperabilidad debe declararse con el tipo que se debe calcular. En otras palabras, dada la definición anterior, el CLR debe realizar algún tipo de determinación en función del tipo estático de NativeStruct
. Supongo que está detectando correctamente que el objeto necesita ser ordenado, pero solo "ordenando" un objeto de cero bytes, el tamaño de NativeStruct
mismo.
Entonces, para que su código funcione para CHARFORMAT2
(y para cualquier otro tipo no blittable que pueda usar), tendrá que volver a declarar que SendMessage
toma un objeto CHARFORMAT2
. Perdón por haberte descarriado en este.
Captcha para la edición anterior:
el whippet
Sí, ¡látigo bien!
Cory,
Esto está fuera de tema, pero veo un problema potencial para ti en la aplicación que parece que estás haciendo.
El control de cuadro de texto enriquecido usa funciones estándar de GDI para medir texto y dibujar texto. ¿Por qué es esto un problema? Porque, a pesar de las afirmaciones de que una fuente TrueType tiene el mismo aspecto en la pantalla que en papel, GDI no coloca caracteres con precisión. El problema es el redondeo.
GDI usa rutinas enteras para medir texto y colocar caracteres. El ancho de cada carácter (y la altura de cada línea, para el caso) se redondea al número entero más cercano de píxeles, sin corrección de errores.
El error se puede ver fácilmente en su aplicación de prueba. Establezca la fuente en Courier New en 12 puntos. Esta fuente de ancho fijo debe espaciar caracteres exactamente 10 por pulgada o 0.1 pulgadas por carácter. Esto debería significar que, dado el ancho de la línea de inicio de 5.5 pulgadas, debería poder caber 55 caracteres en la primera línea antes de que se produzca el ajuste.
ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz123
Pero si lo intentas, verás que el ajuste se produce después de solo 54 caracteres. Además, el 54º personaje y parte del 53º voladizo sobresalen del margen aparente que se muestra en la barra de la regla.
Esto supone que tiene su configuración en 96 DPI estándar (fuentes normales). Si usa 120 ppp (fuentes grandes), no verá este problema, aunque parece que en este caso dimensiona incorrectamente su control. Tampoco es probable que vea esto en la página impresa.
¿Que está pasando aqui? El problema es que 0.1 pulgadas (el ancho de un caracter) es 9.6 pixeles (nuevamente, usando 96 DPI). GDI no espacia los caracteres que usan números de coma flotante, por lo que redondea esto hasta 10 píxeles. Así que 55 caracteres ocupan 55 * 10 = 550 píxeles / 96 DPI = 5.7291666 ... pulgadas, mientras que lo que esperábamos era 5.5 pulgadas.
Si bien esto probablemente será menos notorio en el caso de uso normal de un procesador de textos, existe la posibilidad de que el ajuste de palabras ocurra en diferentes lugares en la pantalla o en la página, o que las cosas no coincidan una vez impresas. lo hicieron en la pantalla. Esto podría ser un problema para usted si se trata de una aplicación comercial en la que está trabajando.
Desafortunadamente, la solución para este problema no es fácil. Significa que tendrá que prescindir del rico control de cuadro de texto, lo que significa una gran molestia de implementar todo lo que hace por usted, que es bastante. También significa que el código de dibujo de texto que deberá implementar se vuelve bastante complicado. Tengo un código que lo hace, pero es demasiado complejo para publicarlo aquí. Sin embargo, puede encontrar útil este ejemplo o este .
¡Buena suerte!
1 clase base abstracta
He tenido algunos casos divertidos donde un parámetro es algo así como ref Guid parent
y la documentación correspondiente dice:
"Puntero a un GUID que especifica el elemento principal. Pase un puntero nulo para usar [insertar algún elemento definido por el sistema] ".
Si null
(o IntPtr.Zero
for IntPtr
parameters) realmente es un parámetro inválido, entonces está bien usar un parámetro ref
, quizás incluso mejor, ya que es más claro exactamente lo que necesita pasar.
Si null
es un parámetro válido, puede pasar ClassType
lugar de ref StructType
. Los objetos de un tipo de referencia ( class
) se pasan como un puntero, y permiten null
.