That amount is really small for .NET. See this doc https://docs.microsoft.com/en-us/cpp/dotnet/calling-native-f... it says “PInvoke has an overhead of between 10 and 30 x86 instructions per call.” That’s barely measurable. Close to the cost of a mispredicted branch. 4x cheaper than a single cache miss.
That’s if you do it correctly, i.e. only marshal value types or arrays/pointers of them, don’t use custom marshallers, etc. Also it’s important to specify correct attributes. To call `int func(const sStruct&)` C++ function, you should specify `[In] ref sStruct` in C#.
Declare a delegate in C#, apply [UnmanagedFunctionPointer] attribute specifying calling convention (on Linux you’ll likely want CallingConvention.Cdecl), pass to native code specifying [MarshalAs( UnmanagedType.FunctionPtr )] in the prototype of the DLL/SO function which accepts that pointer.
This is even more convenient than doing that in C. In C, function pointers are stateless, typical design pattern is pass accompanying `void* context` for the state. Not needed in C#, that delegate can capture whatever it wants, the runtime will do it’s marshalling magic, generating a native function and somehow associating it with the captured state.
The only caveat is lifetime. Function pointers can’t be retained by native code, they’re too simple and don’t have a ref.counter. You must ensure the .NET delegate is not garbage collected while it’s referenced by the native code, or the app will crash.
in .NET stacks are different, the runtime is relatively large, yet interoperability with C is nearly trivial. From my PoV at least, I never worked on compilers, only used them a lot.