4.1. C#/C++ Interface¶
This page is quite techical, in most scenarios you can just look at what already exists!
Note
Native = C++, Managed = C# (think of memory management)
Quick Facts:
Code written in C++ has to be compiled to a shared library (
.dll
on windows) and placed in theAssets/Plugins
folder. This is done by CMake when building.The native functions must be redeclared in C# as
extern
with theDllImport
attribute and can then be called normally. All these declarations are inLibigl/Native.cs
.You can use pointers with the
unsafe
keyword.Managed data structures can have a different layout than native ones. ‘Marshalling’ converts between the two automatically, but can involve expensive copies.
Classes or structs can be shared between but must be declared in both languages.
Beware of the garbage collector. Pin managed data using
fixed
, orGcAlloc
with the pinned option, to prevent the garbage collector from moving/deleting whilst the C++ is executing. This is only for classes (not structs/value types).
4.1.1. C++ Building The Library¶
4.1.1.1. CMake¶
When building the
.dll
is placed in theAssets/Plugins
folder automaticallyNote that the output directory can be set in the CMake cache with the
UNITY_*
variablesSet
CMAKE_VERBOSE
for precise message if something goes wrong in CMakeCurrently only Visual Studio Solutions
.sln
have been tested
4.1.1.2. Rebuilding and Unloading Native Libraries¶
Unity presents the complication that it never unloads a dll once loaded, this prevents write access and rebuilding will fail. A dll is loaded once a function from it is called for the first time.
UnityNativeTool by mcpiroman present a good workaround for this by ‘mocking’ native functions and un/loading the dll manually. This is only done in the editor so builds will be unaffected.
This method allows us to use the normal P/Invoke attribute [DllImport("mylib")]
above
external native function declarations in C#. So there are no changes to our code(!)
This works in edit and play mode and details can be seen in the instance of the DllManipulatorScript
. However, this means the Main
scene must be loaded in order to be able to use the dll.
We also get a callback whenever a library is loaded and unloaded (pre/post) allowing us to initialize
and clean up the native library nicely. This relied on the mysteriously named stubLluiPlugin
.
4.1.1.3. What you need to know:¶
The library is loaded whenever a function is called,
Alt
+Shift
+D
is pressedIt is unloaded when play mode ends, the
DllManipulatorScript
is disabled (OnDisable
) or when manually unloading via the component inspector or the shortcutAlt
+D
When you want to rebuild your library, stop play mode or unload it first in Unity via the shortcut
You can use
[DllImport]
as usualThere are certain limitations to marshalling and similar
We can get callbacks by using the attributes in
UnityNativeTool/scripts/Attributes.cs
, e.g. when the dll is un/loadedUse
[MockNativeDeclarations]
on a class or native function to enable this unloadingThe shortcuts un/load all mocked libraries, if there are several
4.1.2. Debugging¶
C++ or C#: Open the solution in Visual Studio. Debug > Attach To Process...
and select running Unity.exe
. Place breakpoints as usual. Ensure that you build before running so that the source code matches the executing code. For C# debuggin in VS also search online…
Simultaneous C#/C++: VS cannot debug both at the same time, two instances do not work. So current solution is to use Jetbrains Rider to debug the C# side and VS (or CLion) for C++.
Tip
The editor/application will crash if there is a segfault in C++, use Visual Studio to debug. Failed assertions will cause a pop up. When this happens you can attach the debugger and then press Retry to inspect properly.
4.1.3. Calling Native functions¶
4.1.3.1. Do’s and Don’ts¶
Do:
Check that function declarations match exactly by copy-pasting for example
Be careful with references
Label parameters with
in
andout
to improve performanceUse
unsafe
to pass pointers along withUnsafeUtility
Use
NativeArray<T>
when possible along withNativeArrayUnsafeUtility
Keep C#/C++ interface calls to a minimum for a simple interface
Don’t:
Pass large non-blittable types, e.g. matrices, use pointers instead
Have unhandled exceptions. Exceptions should be handled fully in C++ or fully in C#.
Call a C# delegate/function pointer from C++ without checking if it is valid/null.
Lots of problems can arise if this is not the case.
4.1.4. Global Variables/Persistent Memory in C++¶
Anything related to a specific mesh must be part of the MeshState
. However, global variables can be used to store a state between function calls from C#.
Declare these as extern in a header and define them once in a C++. They can be set in the Initialize()
.
Memory allocated with new
in C++ will persist as usual until it is deleted with delete
. Notably, the MeshState
is allocated in C++ when InitializeMesh()
is called. C# can access (read/write but not delete) C++ owned memory.
Note
When the dll is unloaded all memory it allocated must be deleted. This can be done in UnityPluginUnload()
or triggered by a C# destructor, see LibiglMesh.cs and LibiglBehaviour.cs. Notably, when hot reloading this is also the case.
(advanced) When hot reloading (pausing play mode, un/loading the dll) global variables are deleted. Pointers to data allocated with new
are still valid, but the memory cannot be used as the owner dll has been destroyed (effectively a segmentation fault). You cannot simply keep the same data. As such, all persistent data must be serialized and then deserialized if you want the state to survive a hot reload. This has not yet been implemented but could be done with igl::serialize
.
4.1.5. Marshalling¶
Marshalling allows us to pass managed data to a native context. Ensure that you use ‘blittable types’ as much as possible as these do not involve a copy. Generally:
blittable types:
int
,float
, numbers, structs consisting of only these, 1-D arrays of thesenon-blittable types:
string
,bool
, n-D arrays
To pass a struct add the [StructLayout(Sequential)]
attribute to it in C# and redeclare it in C++ in InterfaceTypes.h
with the same variable ordering. in
and out
parameter attributes allow the Marshalling to optimize more. It should match C++ references. For strings use CharSet = Ansi
in DllImport
4.1.6. Calling Managed functions from C++¶
Note
In certain rare scenarios this may be desirable. Think first if this can be avoided. It is possible via function pointer callbacks.
In C#, a delegate (~function pointer type) must be declared and the function to be called. The function must be annotated with
the [MonoPInvokeCallback(typeof(MyDelegate))]
attribute. It must be a static method.
See Scripts/Libigl/NativeCallbacks.cs
and add your callback there.
In C++, declare a function pointer typedef like the delegate, see StringCallback
. The function pointer must use the UNITY_INTERFACE_API
to ensure the __stdcall
C# calling convention is used. Then you declare an instance of the function pointer as extern, see DebugLog
. Finally we must set the pointer when calling Initialize()
and reset to nullptr
in UnityPluginUnload()
. The extern variables need to be properly declared in Native.cpp
.
See source/InterfaceTypes.h
and add your code there.
Warning
Function pointers/callbacks may be invalid or null. Check before invoking them or a crash will occur.
Further reading: Debug.Log example
4.1.7. Further Reading¶
A good simple introduction to P/Invoke
Unrelated and not what you want:
C++/CLI (Microsoft) which is not the same as C++
COM (Microsoft Component Object Model)
CLR (Microsoft Common Language Runtime)
Related and what you are using/looking for:
P/Invoke used by the
DllImportAttribute
(stands for Platform Invoke)
Links:
x86 Calling Conventions for
__stdcall
Unity Macros only the first 20 lines