Xamarin Public Jenkins (auto-signing) 966bba02bb Imported Upstream version 5.2.0.175
Former-commit-id: bb0468d0f257ff100aa895eb5fe583fb5dfbf900
2017-06-07 13:16:24 +00:00

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:

  1. Create a SB of the desired capacity (allocates managed capacity) {1}
  2. Invoke
    1. Allocates a native buffer {2}
    2. Copies the contents if [In] (the default for a StringBuilder parameter)
    3. Copies the native buffer into a newly allocated managed array if [Out] {3} (also the default for StringBuilder)
  3. 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 use CoTaskMemFree by default to free strings or SysStringFree for strings that are marked as UnmanagedType.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.

Windows Data Types for Strings

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 are LayoutKind.Auto

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