DiligentCore icon indicating copy to clipboard operation
DiligentCore copied to clipboard

Add C# bindings

Open DiligentGraphics opened this issue 6 years ago • 1 comments

DiligentGraphics avatar Dec 14 '18 04:12 DiligentGraphics

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

Documentation.

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...

Sam-Izdat avatar Apr 30 '22 12:04 Sam-Izdat

C# bindings for core interfaces added in 9eec045b50e5c005480994e0096c6a785bda412f

TheMostDiligent avatar Aug 07 '23 07:08 TheMostDiligent