DiligentCore
DiligentCore copied to clipboard
Add C# bindings
I'm posting this as a working example of non-trivial C# interop with DiligentCore's BasicMath.hpp
library, which (being a heavily templated library with a lot of operator overloading) presents a challenge.
I should say that, after some testing and working through various problems, I wouldn't recommend using this particular library like this in production. The overhead does add up in terms of performance and there's a few limitations and unresolved issues that make a one-to-one C# implementation quite difficult. The more sensible thing to do would probably be to use a C# linear algebra library and just map the types back to Diligent's API.
SWIG is probably the most complete and mature tool for C# interop with PInvoke. For trivial use cases, like calling simple shared library functions or setting up a few callback delegates, it's probably better and easier to just set up the marshalling manually. SWIG takes in 'interface' files, exposing all or part of a C++ header, and compiles both C++ and C# glue, taking care of marshalling automatically. It also takes care of all the __declspec(dllexport)
stuff automatically, without modifying your code.
This should, in principle, be cross platform, but this was tested on:
OS: Windows 10
Compiler: MSVC (Visual Studio 2022 17.1.0)
.NET 6
C++ 17
cmake version 3.22.2
SWIG 4.0.2
Diligent Core last commit 7f5e6a6c219ffb581491d947d80aabe61bc846aa (Apr 14 2022)
CMake
Documentation. (UseSWIG)
CMake has SWIG integration built-in. SWIG binaries are needed and can be downloaded here or built from source.
Here is a very simple, minimal C++/C# interop example.
Some CMakeLists.txt boilerplate:
if (MSVC)
set(SWIG_DIR "${PROJECT_SOURCE_DIR}/deps/win32/swigwin-4.0.2/Lib")
set(SWIG_EXECUTABLE "${PROJECT_SOURCE_DIR}/deps/win32/swigwin-4.0.2/swig.exe")
endif (MSVC)
find_package(SWIG REQUIRED)
include(${SWIG_USE_FILE})
set(CMAKE_SWIG_FLAGS "")
For convenience:
set (SWIGIFS
src/Diligent.i
#any/other/interfaces.i
)
foreach(FILE ${SWIGIFS})
set_property(SOURCE ${FILE} PROPERTY USE_TARGET_INCLUDE_DIRECTORIES TRUE)
set_property(SOURCE ${FILE} PROPERTY CPLUSPLUS ON)
set_source_files_properties(${FILE}
PROPERTIES SWIG_FLAGS "-includeall"
)
list(APPEND SWIGFILES ${FILE})
endforeach(FILE)
Since compiling a SWIG interface library separately from the engine library and linking them would mean dragging stl types across the DLL boundary, I opted to compile the engine with SWIG. That means replacing add_library
with:
swig_add_library(${APP_NAME}
TYPE SHARED
LANGUAGE CSharp
SOURCES "${SOURCE}" "${INCLUDE}" "${ASSETS}" "${SWIGFILES}"
OUTPUT_DIR "${${APPLICATION_NAME}_NET_DIR}" # CS glue dir
OUTFILE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/src/SWIG" # CPP glue dir
)
Otherwise, the engine library can be compiled as a normal shared library and then linked to a separate SWIG library for 'translation' to C#, either with swig_link_libraries
or target_link_libraries
if UseSWIG_TARGET_NAME_PREFERENCE
is set to STANDARD
.
With USE_TARGET_INCLUDE_DIRECTORIES
set to TRUE
above target_include_directories
will be used (or propagated) to SWIG interfaces. Otherwise, there's some SWIG-specific ways of specifying them in the documentation.
SWIG
SWIG will compile InterfaceName.i
files. Here is the interface for BasicMath.hpp
-- just the Vector2
type for brevity.
%module Diligent
// This is where to include all the headers for SWIG:
%{
#include <cmath>
#include <algorithm>
#include <iostream>
#include "HashUtils.hpp"
#include "BasicMath.hpp"
%}
// And below is what we want to expose to C#:
namespace Diligent {
// The order of things in this interface can be important in non-obvious ways.
// For example, the typemap must appear before any of its types and, in this case,
// needs to be in the template class. The typemap step may be unnecessary with a
// simpler interface.
template <class T> struct Vector2
{
using uint = uint32_t;
// This typemap is injected into the C# side of the interop layer and maps C++ operator overloads
// to C# ones. The renamed "__X" operators map to C++ overload functions through PInvoke.
// $typemap(cstype, T vec) turns e.g. into "float2" or "int2" while T is, of course, "float" and "int".
// Although it feels quite hacky, $csclassname can also be used here instead of using cstypes.
%typemap(cstype) float vec "float2"
%typemap(cstype) double vec "double2"
%typemap(cstype) int vec "int2"
%typemap(cstype) uint vec "uint2"
%typemap(cscode) Vector2%{
public static $typemap(cstype, T vec) operator+($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2)
=> v1.__add(v2); // vec + vec
public static $typemap(cstype, T vec) operator-($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2)
=> v1.__sub(v2); // vec - vec
public static $typemap(cstype, T vec) operator-($typemap(cstype, T vec) v1)
=> v1.__neg(); // -vec
public static $typemap(cstype, T vec) operator*($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2)
=> v1.__mul(v2); // vec * vec
public static $typemap(cstype, T vec) operator*($typemap(cstype, T vec) v1, T v2)
=> v1.__mul(v2); // vec * scaler
public static $typemap(cstype, T vec) operator/($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2)
=> v1.__div(v2); // vec / vec
public static $typemap(cstype, T vec) operator/($typemap(cstype, T vec) v1, T v2)
=> v1.__div(v2); // vec / scaler
public static $typemap(cstype, T vec) operator>($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2)
=> v1.__gt(v2); // (each) vec.member > vec.member
public static $typemap(cstype, T vec) operator<($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2)
=> v1.__lt(v2); // (each) vec.member < vec.member
public static $typemap(cstype, T vec) operator>=($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2)
=> v1.__gte(v2); // (each) vec.member >= vec.member
public static $typemap(cstype, T vec) operator<=($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2)
=> v1.__lte(v2); // (each) vec.member <= vec.member
// These had problems. See below.
//public static bool operator==($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2) {
//public static bool operator!=($typemap(cstype, T vec) v1, $typemap(cstype, T vec) v2)
%}
// Overloaded operators must be renamed in order to be mapped to C# operators.
// Some operators are not available or work differently than in C++.
// The += and -= compound assignment ones are not allowed, but in this case we get
// them for free. Index operators are not allowed but can be replaced with an indexer.
// Assignment operators, as a general rule, are not available.
//
// The equality operator is excluded because, for reasons I couldn't figure out,
// it throws a null pointer exception if it can ever evaluate to true. Without digging
// deeper, I'm not sure if this is a SWIG bug (or lack of C# specific implementation)
// or just a mistake on my part.
//
// Per MS C# docs:
//
// Compound assignment operators cannot be explicitly overloaded.
// However, when you overload a binary operator, the corresponding
// compound assignment operator, if any, is also implicitly overloaded.
// For example, += is evaluated using + , which can be overloaded.
// . . .
// Element access is not considered an overloadable operator
// . . .
// [Assignment] operators cannot be overloaded
//
// https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/operators/operator-overloading
%rename(__eq) operator==; // equality
%rename(__ineq) operator!=; // inequality
%rename(__gt) operator>; // greater
%rename(__lt) operator<; // lesser
%rename(__gte) operator>=; // greater or equal
%rename(__lte) operator<=; // lesser or equal
%rename(__add) operator+; // addition
%rename(__sub) operator-; // subtraction
%rename(__neg) operator-(); // negative (unary minus)
%rename(__mul) operator*; // multiplication
%rename(__div) operator/; // division
union
{
struct
{
T x;
T y;
};
struct
{
T r;
T g;
};
struct
{
T u;
T v;
};
};
Vector2(const Vector2&);
Vector2(Vector2&&);
Vector2 operator-(const Vector2<T>& right);
Vector2 operator-();
Vector2 operator+(const Vector2<T>& right);
Vector2 operator*(T s);
Vector2 operator*(const Vector2& right);
Vector2 operator/(const Vector2& right);
Vector2 operator/(T s);
bool operator==(const Vector2& right);
bool operator!=(const Vector2& right);
Vector2 operator<(const Vector2& right);
Vector2 operator>(const Vector2& right);
Vector2 operator<=(const Vector2& right);
Vector2 operator>=(const Vector2& right);
// Omitted so this interface compiles on its own:
//Vector2 operator*(const Matrix2x2<T>& m);
Vector2();
explicit Vector2(T s);
Vector2(T _x, T _y);
// Compiles fine, but doesn't seem to be strictly necessary here:
//template <typename Y>
//Vector2 MakeVector(const Y& vals);
//
//template <typename Y>
//Vector2 MakeVector(const Y& vals);
//template <typename Y>
//Vector2<Y> Recast();
};
// I could not get uint vectors to work. One of the problems had something to do
// with the negation operator in the template above applied to an unsigned type,
// but there's something else going on here. Bool vectors may need special handling,
// since they can't be easily typemapped and templated with numeric types.
// There's probably some way to do this, and it's probably annoying.
%template(float2) Vector2<float>;
%template(double2) Vector2<double>;
%template(int2) Vector2<int>;
//%template(uint2) Vector2<uint>;
typedef Vector2<float> float2;
typedef Vector2<double> double2;
typedef Vector2<int> int2;
//typedef Vector2<uint> uint2;
}
C#
Now, in a .NET 6 project, we can do:
float2 foo = new float2(0.34f, 0.98f);
float2 bar = new float2(0.51f, 0.35f);
foo *= -bar;
etc...
C# bindings for core interfaces added in 9eec045b50e5c005480994e0096c6a785bda412f