4 Interop guidelines
Igor Velikorossov редактировал(а) эту страницу 2020-04-25 22:58:25 +10:00

P/Invoke and COM imports

READ First: Native interoperability best practices


Q: Should interop imports be declared as public?

A: No, interop imports should all be internal to match the CoreCLR/FX convention.


Q: Should interop imports use BOOL vs bool?

A: BOOL requires no marshaling, bool does.
bool is marshaled as BOOL (UnmangedType.Bool) in P/Invokes, and VARIANT_BOOL (UnmanagedType.VariantBool) in COM imports. .NET bool is never blitted, it is always marshalled, and by default as 'int'. You can add the [MarshalAs] to tell it otherwise, or use our "native" wrapper enums.


Q: What's the difference between sizeof(T) and Marshal.SizeOf<T>()?

A: The differences between blitting structs and classic marshalling.

  • Marshal.SizeOf for classic marshalling size
  • Marshal.OffsetOf for classic marshalling offsets
  • sizeof keyword for blitting size
  • pointer arithmetic for blitting offsets

Example where it makes a difference:

public struct Sample
{
    public int a;
    public bool b;
    public short c;
}

var sample = new Sample();
var blittingSize = sizeof(Sample);                              // 8
var blittingOffset = (byte*)&sample.c - (byte*)&sample;         // 6
var marshalSize = Marshal.SizeOf<Sample>();                     // 12
var marshalOffset = Marshal.OffsetOf<Sample>(nameof(Sample.c)); // 8

Q: When to declare Charset in DllImportAttributes?

A: If there aren't any strings or chars then Charset isn't useful.
Charset is Ansi by default so we should always be setting it as we'll almost always want it and we'll want to be super explicit when we don't. For the APIs that have a A/W it's best to be explicit to avoid the probing.


Q: When to use the ExactSpelling field and an explicit W in DllImportAttributes?

A: For the APIs that have a A/W it's best to be explicit to avoid the probing. As a general rule, we set ExactSpelling=true for all p/invokes.


Q: What's the difference between MarshalAs for .IUnknown, .IDispatch and .Interface?

A: Everything in COM derives from IUnknown. Only some classes support IDispatch (which derives from IUnknown).
The documentation goes into detail here, but the gist is that object by default marshals as VARIANT, which is almost always not what we want.
If we don't use object and use a ComImport interface you don't need anything.


Q: So what is the practical difference then between things like .Interface, .IUnknown and .IDispatch marshalling? Surely if all interfaces implement IUnknown then there is no difference?

A: Which of the 3 you use depends on the C signature:

  • C signature is IUnknown, use IUnknown
  • C signature is IDispatch, use IDispatch
  • C signature is some interface, use Interface and make sure the C# type is actually a matching COM import

IUnknown vs. IDispatch depends on the C signature. Some methods explicitely have an IDispatch in the signature, so if you pass an IUnknown you get an access violation if the method actually calls into it because it has less methods than IDispatch. As said above not all COM objects implement IDispatch so I'd only use it when the signature actually demands it, though passing an IDispatch when only IUnknown is demanded doesn't harm anything (assuming the thing you pass actually implements IDispatch).

When a C signature demands an explicit interface (and not just an IUnknown or IDispatch) then you as the caller need to do the QueryInterface for that interface, which is when you need to use Interface - it will look at the C# type (which must be a ComImport interface) and QueryInterface for the GUID on the interface. If you don't do that and use IUnknown or IDispatch then you'll get access violations when the called method tries to call anything on the interface.

Note that the reverse is less problematic, for example you can always pass a more specific interface when the caller is just expecting IUnknown.


Q: In what scenario do we need to decorate members with MarshalAs(Interface)?

A: You only need to do it for object, which marshals as variant by default. If you're returning/passing a COM interface it marshals as interface. object its equivalent to [MarshalAs(IUnknown)] because you can't specify the IID to query for:

This member produces the same behavior as IUnknown when you apply it to the Object data type.


Q: Do I need to keep a callback alive while it is marshaled to a native call?

A: Strictly speaking it is not necessary if a delegate doesn't outlive a method call. Marshaling a Delegate as a Callback Method says:

When you use a delegate inside a call, the common language runtime protects the delegate from being garbage collected for the duration of that call.

However we do want to use GC.KeepAlive until after the native call is returned. See for more details.