I started foraying a bit in to macOS platform invocation with .NET Core and C#. For the most part, it works exactly like it did with Windows. However, there are some important differences between Windows’ native APIs and macOS’.
The first is calling convention. Win32 APIs are typically going to be
on 32-bit or the AMD64 calling convention on 64-bit. That may not be true for 3rd
party libraries, but it is true for most (but not all) Win32 APIs.
MacOS’ OS provided libraries are overwhelmingly
cdecl and have a similar but
different calling convention for AMD64 (the same as the System V ABI).
For the most part, that doesn’t affect platform invoke signatures that much. However if you are getting in to debugging with LLDB, it’s something to be aware of.
It does mean that you need to set the
CallingConvention appropriately on the
DllImportAttribute. For example:
[DllImport("libcrypto.41", EntryPoint = "TS_REQ_set_version", CallingConvention = CallingConvention.Cdecl) ]
Another point is that MacOS uses the LP64 memory model, whereas Windows uses the LLP64 for types.
A common Win32 platform invocation mistake is trying to marshal a native
to a managed
long. The native
long in Win32 is 32bits, whereas in .NET it is
64-bit. Mismatching them will do strange things to the stack. In Win32 platform
invocation, a native
long gets marshalled as an
int. Win32 will use
long long or
int64_t for 64-bit types.
MacOS is different. It’s
long type is platform dependent. That is, on 32-bit
long type is 32-bit, and on 64-bit it is 64-bit. In that regard,
long type is most accurately marshalled as an
IntPtr. The alternative
is to provide two different platform invoke signatures and structs and use the
appropriate one depending on the platform.
Keep in mind with MacOS, MacOS is exclusively 64-bit now. It’s still possible that one day your code will run 32-bit on a Mac as it is still capable of running 32-bit. At the time of writing even .NET Core itself doesn’t support running 32-bit on a Mac.
[DllImport("libcrypto.41", EntryPoint = "TS_REQ_set_version", CallingConvention = CallingConvention.Cdecl) ] public static extern int TS_REQ_set_version ( [param: In] TsReqSafeHandle a, [param: In, MarshalAs(UnmanagedType.SysInt)] IntPtr version );
IntPtr for the
long type is a bit of a pain since for, whatever
reason, C# doesn’t really treat it like a numeric type. You cannot create
IntPtr cleanly, instead having to do something like
A final possibility is to make a native shim that coerces the data types to
something consistent, like
int32_t and have a shim per architecture.
Another point of difference is string encoding. Windows vastly prefers to use Unicode and ANSI strings (W or A), where MacOS libraries will frequently use UTF8. The easiest thing to do here is to marshal them as pointers, unfortunately.
Overall, it’s not too much different. Pay attention to the calling convention and be aware of LP64 over LLP64.