Former-commit-id: bb0468d0f257ff100aa895eb5fe583fb5dfbf900
13 KiB
P/Invokes
This document extends the Interop Guidelines to provide more specific guidelines, notes, and resources for defining P/Invokes.
Attributes
Implicit attributes applied to parameter and return values:
Implicit Attribute | |
---|---|
parameter | [In] |
out parameter |
[Out] |
ref parameter |
[In],[Out] |
return value | [Out] |
[DllImport()]
1 attribute settings:
Setting | Recommendation | Details |
---|---|---|
PreserveSig |
keep default | When this is explicitly set to false (the default is true), failed HRESULT return values will be turned into Exceptions (and the return value in the definition becomes null as a result). |
SetLastError |
as per API | Set this to true (default is false) if the API uses GetLastError and use Marshal.GetLastWin32Error to get the value. If the API sets a condition that says it has an error, get the error before making other calls to avoid inadvertently having it overwritten. |
ExactSpelling |
true |
Set this to true (default is false) and gain a slight perf benefit as the framework will avoid looking for an "A" or "W" version. (See NDirectMethodDesc::FindEntryPoint). |
CharSet |
Explicitly use CharSet.Unicode or CharSet.Ansi when strings are present in the definition |
This specifies marshalling behavior of strings and what ExactSpelling does when false . Be explicit with this one as the documented default is CharSet.Ansi . Note that CharSet.Ansi is actually UTF8 on Unix (CharSet.Utf8 is coming). Most of the time Windows uses Unicode while Unix uses UTF8. |
Strings
When the CharSet is Unicode or the argument is explicitly marked as [MarshalAs(UnmanagedType.LPWSTR)]
and the string is passed by value (not ref
or out
) the string will be be pinned and used directly by native code (rather than copied).
Remember to mark the [DllImport]
as Charset.Unicode
unless you explicitly want ANSI treatment of your strings.
[AVOID] StringBuilder
marshalling always creates a native buffer copy (see ILWSTRBufferMarshaler
). As such it can be extremely inefficient. Take the typical
scenario of calling a Windows API that takes a string:
- Create a SB of the desired capacity (allocates managed capacity) {1}
- Invoke
- Allocates a native buffer {2}
- Copies the contents if
[In]
(the default for aStringBuilder
parameter) - Copies the native buffer into a newly allocated managed array if
[Out]
{3} (also the default forStringBuilder
)
ToString()
allocates yet another managed array {4}
That is {4} allocations to get a string out of native code. The best you can do to limit this is to reuse the StringBuilder
in another call but this still only saves 1 allocation. It is much better to use and cache a character buffer from ArrayPool
- you can then get down to just the allocation for the ToString()
on subsequent calls.
The other issue with StringBuilder
is that it always copies the return buffer back up to the first null. If the passed back string isn't terminated or is a double-null-terminated string your P/Invoke is incorrect at best.
If you do use StringBuilder
one last gotcha is that the capacity does not include a hidden null which is always accounted for in interop. It is pretty common for people to get this wrong as most APIs want the size of the buffer including the null. This can result in wasted/unnecessary allocations.
[USE] Char arrays from ArrayPool
or StringBuffer
.
Default Marshalling for Strings
Windows Specific
For
[Out]
strings the CLR will useCoTaskMemFree
by default to free strings orSysStringFree
for strings that are marked asUnmanagedType.BSTR
.
For most APIs with an output string buffer:
The passed in character count must include the null. If the returned value is less than the passed in character count the call has succeeded and the value is the number of characters without the trailing null. Otherwise the count is the required size of the buffer including the null character.
- Pass in 5, get 4: The string is 4 characters long with a trailing null.
- Pass in 5, get 6: The string is 5 characters long, need a 6 character buffer to hold the null.
Booleans
Booleans are easy to mess up. The default marshalling for P/Invoke is as the Windows type BOOL
, where it is a 4 byte value. BOOLEAN
, however, is a single byte. This can lead to hard to track down bugs as half the return value will be discarded, which will only potentially change the result. For BOOLEAN
attributing bool
with either [MarshalAs(UnmanagedType.U1)]
or [MarshalAs(UnmanagedType.I1)]
will work as TRUE
is defined as 1
and FALSE
is defined as 0
. U1
is technically more correct as BOOLEAN
is defined as an unsigned char
.
bool
is not a blittable type (see blitting below). As such, when defining structs it is recommended to use Interop.BOOL.cs
for BOOL
to get the best performance.
Default Marshalling for Boolean Types
Guids
Guids are usable directly in signatures. When passed by ref they can either be passed by ref
or with the [MarshalAs(UnmanagedType.LPStruct)]
attribute.
Guid | By ref Guid |
---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
[MarshalAs(UnmanagedType.LPStruct)]
should only be used for by ref Guids.
Common Data Types
Windows | C | C# | Alternative |
---|---|---|---|
BOOL |
int |
int |
bool |
BOOLEAN |
unsigned char |
byte |
[MarshalAs(UnmanagedType.U1)] bool |
BYTE |
unsigned char |
byte |
|
CHAR |
char |
sbyte |
|
UCHAR |
unsigned char |
byte |
|
SHORT |
short |
short |
|
CSHORT |
short |
short |
|
USHORT |
unsigned short |
ushort |
|
WORD |
unsigned short |
ushort |
|
ATOM |
unsigned short |
ushort |
|
INT |
int |
int |
|
LONG |
long |
int |
|
ULONG |
unsigned long |
uint |
|
DWORD |
unsigned long |
uint |
|
LARGE_INTEGER |
__int64 |
long |
|
LONGLONG |
__int64 |
long |
|
ULONGLONG |
unsigned __int64 |
ulong |
|
ULARGE_INTEGER |
unsigned __int64 |
ulong |
|
UCHAR |
unsigned char |
byte |
|
HRESULT |
long |
int |
Signed Pointer Types (IntPtr ) |
Unsigned Pointer Types (UIntPtr ) |
---|---|
HANDLE |
WPARAM |
HWND |
UINT_PTR |
HINSTANCE |
ULONG_PTR |
LPARAM |
SIZE_T |
LRESULT |
|
LONG_PTR |
|
INT_PTR |
Windows Data Types
Data Type Ranges
Blittable Types
Blittable types are types that have the same representation for native code. As such they do not need to be converted to another format to be marshalled to and from native code, and as this improves performance they should be preferred.
Blittable types:
byte
,sbyte
,short
,ushort
,int
,uint
,long
,ulong
,single
,double
- non-nested one dimensional arrays of blittable types (e.g.
int[]
) - structs and classes with fixed layout that only have blittable types for instance fields
- fixed layout requires
[StructLayout(LayoutKind.Sequential)]
or[StructLayout(LayoutKind.Explicit)]
- structs are
LayoutKind.Sequential
by default, classes areLayoutKind.Auto
- fixed layout requires
NOT blittable:
bool
SOMETIMES blittable:
char
,string
When blittable types are passed by reference they are simply pinned by the marshaller instead of being copied to an intermediate buffer. (Classes are inherently passed by reference, structs are passed by reference when used with ref
or out
.)
char
is blittable in a one dimensional array or if it is part of a type that contains it is explicitly marked with [StructLayout]
with CharSet = CharSet.Unicode
.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
public char c;
}
string
is blittable if it isn't contained in another type and it's being passed as an argument that is marked with [MarshalAs(UnmanagedType.LPWStr)]
or the [DllImport]
has CharSet = CharSet.Unicode
set.
You can see if a type is blittable by attempting to create a pinned GCHandle
. If the type is not a string or considered blittable GCHandle.Alloc
will throw an ArgumentException
.
Blittable and Non-Blittable Types
Default Marshalling for Value Types
Keeping Managed Objects Alive
GC.KeepAlive()
will ensure an object stays in scope until the KeepAlive method is hit.
HandleRef
allows the marshaller to keep an object alive for the duration of a P/Invoke. It can be used instead of IntPtr
in method signatures. SafeHandle
effectively replaces this class and should be used instead.
GCHandle
allows pinning a managed object and getting the native pointer to it. Basic pattern is:
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();
Pinning is not the default for GCHandle
. The other major pattern is for passing a reference to a managed object through native code back to managed code (via a callback, typically). Here is the pattern:
GCHandle handle = GCHandle.Alloc(obj);
SomeNativeEnumerator(callbackDelegate, GCHandle.ToIntPtr(handle));
// In the callback
GCHandle handle = GCHandle.FromIntPtr(param);
object managedObject = handle.Target;
// After the last callback
handle.Free();
Don't forget that GCHandle
needs to be explicitly freed to avoid memory leaks.
Structs
Managed structs are created on the stack and aren't removed until the method returns. By definition then, they are "pinned" (it won't get moved by the GC). You can also simply take the address in unsafe code blocks if native code won't use the pointer past the end of the current method.
Blittable structs are much more performant as they they can simply be used directly by the marshalling layer. Try to make structs blittable (for example, avoid bool
). See the "Blittable Types" section above for more details.
If the struct is blittable use sizeof()
instead of Marshal.SizeOf<MyStruct>()
for better performance. As mentioned above, you can validate that the type is blittable by attempting to create a pinned GCHandle
. If the type is not a string or considered blittable GCHandle.Alloc
will throw an ArgumentException
.
Pointers to structs in definitions must either be passed by ref
or use unsafe
and *
.
Other References
MarshalAs Attribute
GetLastError and managed code
Copying and Pinning
Marshalling between Managed and Unmanaged Code (MSDN Magazine January 2008) This is a .chm download