使用 %typemap 的 SWIG 和多态 C#

pdm*_*011 3 c# swig

我想在 SWIG 生成的 C# 项目中支持向下转换。

我有一系列std::shared_ptr从公共基础继承的 C++包装类模板。任何IBasePtr在 C++ 代码中返回基类 ( ) 的 C++ 方法都会导致生成的方法返回一个具体IBase对象,该对象与我实际尝试获取的对象无关。此处的博客文章通过插入自定义代码以根据对象类型元数据执行向下转换来解决这个确切的问题。

C++(为了说明而简化):

IBase.h:

namespace MyLib
{
    enum DataTypes
    {
        Float32,
        Float64,
        Integer32
    };

    typedef std::tr1::shared_ptr<IBase> IBasePtr;

    class IBase
    {
    public:
        virtual ~IBase() {}

        DataTypes DataType() const = 0;
    };
}
Run Code Online (Sandbox Code Playgroud)

CDerived.h:

#include "IBase.h"

namespace MyLib
{
    template <class T>
    class CDerived : public IBase
    {
    public:
        CDerived(const DataTypes dataType)
        :
        m_dataType(dataType)
        {}

        DataTypes DataType() const
        {
            return m_dataType;
        }

    private:
        DataTypes m_dataType;
    };
}
Run Code Online (Sandbox Code Playgroud)

CCaller.h:

#include "IBase.h"

namespace MyLib
{
    class CCaller
    {
    public:
        IBasePtr GetFloatObject()
        {
            //My code doesn't really do this - type identification is handled more elegantly, it's just to illustrate.
            base = IBasePtr(new CDerived<float>(Float32));
            return base;
        }

        IBasePtr GetDoubleObject()
        {
            //My code doesn't really do this - type identification is handled more elegantly, it's just to illustrate.
            base = IBasePtr(new CDerived<double>(Float64));
            return base;
        }
    private:
        IBasePtr base;
    };
}
Run Code Online (Sandbox Code Playgroud)

SWIG 接口:

%module SwigWrapper

%include "typemaps.i"
%include <cpointer.i>

#define SWIG_SHARED_PTR_SUBNAMESPACE tr1
%include <std_shared_ptr.i>

%shared_ptr(MyLib::IBase) 
%shared_ptr(MyLib::CDerived< float >)
%shared_ptr(MyLib::CDerived< double >)
%shared_ptr(MyLib::CDerived< int >)

%typemap(ctype, out="void *") MyLib::IBasePtr &OUTPUT "MyLib::IBasePtr *"
%typemap(imtype, out="IntPtr") MyLib::IBasePtr &OUTPUT "out IBase"
%typemap(cstype, out="$csclassname") MyLib::IBasePtr &OUTPUT "out IBase"
%typemap(csin) MyLib::IBasePtr &OUTPUT "out $csinput"
%typemap(in) MyLib::IBasePtr &OUTPUT

%{ $1 = ($1_ltype)$input; %}

%apply MyLib::IBasePtr &OUTPUT { MyLib::IBasePtr & base };

%{
#include "IBase.h"
#include "CDerived.h"
#include "CCaller.h"
using namespace std;
using namespace MyLib;
%}

namespace MyLib
{
    typedef std::tr1::shared_ptr<IBase> IBasePtr;

    %template (CDerivedFloat) CDerived<float>;
    %template (CDerivedDouble) CDerived<double>;
    %template (CDerivedInt) CDerived<int>;
}

%typemap(csout, excode=SWIGEXCODE)
IBase
IBasePtr
MyLib::IBase,
MyLib::IBasePtr
{
    IntPtr cPtr = $imcall;
    $csclassname ret = ($csclassname) $modulePINVOKE.InstantiateConcreteClass(cPtr, $owner);$excode
    return ret;
}

%pragma(csharp) imclasscode=%{
    public static IBase InstantiateConcreteClass(IntPtr cPtr, bool owner)
    {
        IBase ret = null;
        if (cPtr == IntPtr.Zero)
        {
            return ret;
        }

        int dataType = SwigWrapperPINVOKE.IBase_DataType(new HandleRef(null, cPtr));
        DataTypes dt = (DataTypes)dataType;

        switch (dt)
        {
            case DataTypes.Float32:
                ret = new CDerivedFloat(cPtr, owner);
                break;
            case DataTypes.Float64:
                ret = new CDerivedDouble(cPtr, owner);
                break;
            case DataTypes.Integer32:
                ret = new CDerivedInt(cPtr, owner);
                break;
            default:
                System.Diagnostics.Debug.Assert(false,
                String.Format("Encountered type '{0}' that is not a supported MyLib concrete class", dataType.ToString()));
                break;
        }   
        return ret;
    }
%}
Run Code Online (Sandbox Code Playgroud)

我挣扎的部分是使用 SWIG 的%typemap命令。 %typemap旨在指示 SWIG 映射输入和目标类型,在我的情况下通过代码执行显式转换。方法 InstantiateConcreteClass 已生成,但没有对其的引用。

我错过了重要的一步吗?我想知道是否由于shared_ptr在本机代码中使用了一些额外的复杂性,但我认为情况并非如此。

Fle*_*exo 5

您的示例的问题似乎是您为输入编写了类型映射,但这本身似乎没有意义,因为重要的部分是在创建事物时正确使用类型,而不是将它们用作输入。就输出参数而言,这个答案的后半部分解决了这个问题,但是使用类型映射作为参数也存在错误。

我已经稍微简化了您的示例,并使其完整且有效。我必须补充的主要内容是一个“工厂”函数,它创建派生实例,但将它们作为基本类型返回。(如果您只是new直接使用它们创建它们,则不需要)。

我合并了你的头文件并实现了一个内联工厂作为 test.h:

#include <memory>

enum DataTypes {
    Float32,
    Float64,
    Integer32
};

class IBase;

typedef std::shared_ptr<IBase> IBasePtr;

class IBase {
public:
    virtual ~IBase() {}
    virtual DataTypes DataType() const = 0;
};

template <typename T> struct DataTypesLookup;
template <> struct DataTypesLookup<float> { enum { value = Float32 }; };
template <> struct DataTypesLookup<double> { enum { value = Float64 }; };
template <> struct DataTypesLookup<int> { enum { value = Integer32 }; };

template <class T>
class CDerived : public IBase {
public:
    CDerived() : m_dataType(static_cast<DataTypes>(DataTypesLookup<T>::value)) {}

    DataTypes DataType() const {
        return m_dataType;
    }
private:
    const DataTypes m_dataType;
};

inline IBasePtr factory(const DataTypes type) {
    switch(type) {
    case Integer32:
        return std::make_shared<CDerived<int>>();
    case Float32:
        return std::make_shared<CDerived<float>>();
    case Float64:
        return std::make_shared<CDerived<double>>();
    }
    return IBasePtr();
}
Run Code Online (Sandbox Code Playgroud)

这里的主要变化是添加了一些模板元编程,以允许仅从模板参数中IBase查找正确的值并将其更改为 const。我这样做是因为让实例对它们的类​​型撒谎是没有意义的——它只设置了一次,不应该进一步暴露。DataTypeTDataTypeCDerived

鉴于此,我可以编写一些 C# 来展示我打算如何在包装后使用它:

using System;

public class HelloWorld {
    static public void Main() {
        var result = test.factory(DataTypes.Float32);
        Type type = result.GetType();
        Console.WriteLine(type.FullName);
        result = test.factory(DataTypes.Integer32);
        type = result.GetType();
        Console.WriteLine(type.FullName);
    }
}
Run Code Online (Sandbox Code Playgroud)

基本上,如果我的类型映射正在工作,我们将使用该DataType成员透明地test.factory返回一个匹配派生 C++ 类型的 C# 代理,而不是一个只知道基本类型的代理。

还要注意,这里因为我们有工厂,我们也可以修改它的包装以使用输入参数来确定输出类型,但这比DataType在输出上使用更不通用。(对于工厂方法,我们必须为正确的包装编写每个函数而不是每个类型的代码)。

我们可以为此示例编写一个 SWIG 接口,它与您的和引用的博客文章基本相似,但有一些更改:

%module test

%{
#include "test.h"
%}

%include <std_shared_ptr.i>

%shared_ptr(IBase) 
%shared_ptr(CDerived<float>)
%shared_ptr(CDerived<double>)
%shared_ptr(CDerived<int>)

%newobject factory; // 1

%typemap(csout, excode=SWIGEXCODE) IBasePtr { // 2
    IntPtr cPtr = $imcall;
    var ret = $imclassname.make(cPtr, $owner);$excode // 3
    return ret;
}

%include "test.h" // 4

%template (CDerivedFloat) CDerived<float>;
%template (CDerivedDouble) CDerived<double>;
%template (CDerivedInt) CDerived<int>;

%pragma(csharp) imclasscode=%{
    public static IBase make(IntPtr cPtr, bool owner) {
        IBase ret = null;
        if (IntPtr.Zero == cPtr) return ret;

        ret = new IBase(cPtr, false); // 5
        switch(ret.DataType()) {
            case DataTypes.Float32:
                ret = new CDerivedFloat(cPtr, owner);
                break;
            case DataTypes.Float64:
                ret = new CDerivedDouble(cPtr, owner);
                break;
            case DataTypes.Integer32:
                ret = new CDerivedInt(cPtr, owner);
                break;
            default:
                if (owner) ret = new IBase(cPtr, owner); // 6
                break;
        };
        return ret;
    }
%}
Run Code Online (Sandbox Code Playgroud)

该类型图中的注释突出显示了 6 个显着更改:

  1. 我们告诉 SWIG 返回的对象factory是新的,即所有权从 C++ 转移到 C#。(这会导致owner布尔值正确设置)
  2. 我的 typemap 是一个 csout typemap,这是唯一需要的。
  3. 与我使用的链接的教程相比$imclassname,它$modulePINVOKE始终正确扩展为或等效。
  4. %include直接使用我的头文件以避免不必要地重复我自己。
  5. 我没有接触包装器的内部工作原理,而是IBase直接创建了一个临时实例,它允许我以更简洁的方式访问枚举值。临时实例的所有权设置为 false,这意味着我们delete在处理它时永远不会错误地处理底层 C++ 实例。
  6. IBase如果由于某种原因无法找出派生类型,我选择让默认路径通过 switch 语句返回一个不知道派生类型的实例。

根据您在问题中显示的内容,实际上您最关心的是输出参考参数。如果没有 shared_ptr 角度,这根本不起作用。包装它的最简单的解决方案是使用%inline%extend在 SWIG 中编写要使用的函数的替代版本,该版本不通过引用参数传递输出。

然而,我们也可以在 C# 方面自然地进行这项工作,并使用更多类型映射。您使用 OUTPUT 和%apply您展示的样式类型映射走在正确的轨道上,但是我认为您没有完全正确地理解它们。我已经扩展了我的例子来涵盖这一点。

首先,虽然我不太喜欢使用这样的函数,但我添加factory2到 test.h 中:

using System;

public class HelloWorld {
    static public void Main() {
        var result = test.factory(DataTypes.Float32);
        Type type = result.GetType();
        Console.WriteLine(type.FullName);
        result = test.factory(DataTypes.Integer32);
        type = result.GetType();
        Console.WriteLine(type.FullName);
    }
}
Run Code Online (Sandbox Code Playgroud)

这里要注意的关键是,到我们调用时,factory2我们必须有一个对IBasePtr( std::shared_ptr<IBase>)的有效引用,即使 shared_ptr 为空。由于您使用outref是 C#而不是in 您,我们需要std::shared_ptr在调用实际发生之前安排制作一个临时的 C++ 。一旦调用发生,我们希望将其传递回make我们之前为更简单的情况编写的静态函数。

我们将不得不仔细研究 SWIG 如何处理智能指针来使这一切正常工作。

其次,我的 SWIG 界面最终添加了:

%typemap(cstype) IBasePtr &OUTPUT "out $typemap(cstype,$1_ltype)"
%typemap(imtype) IBasePtr &OUTPUT "out IntPtr" // 1
// 2:
%typemap(csin,pre="    IntPtr temp$csinput = IntPtr.Zero;",
              post="    $csinput=$imclassname.make(temp$csinput,true);") 
    IBasePtr &OUTPUT "out temp$csinput" 
// 3:
%typemap(in) IBasePtr &OUTPUT {
    $1 = new $*1_ltype;
    *static_cast<intptr_t*>($input) = reinterpret_cast<intptr_t>($1);
}

%apply IBasePtr &OUTPUT { IBasePtr& result }
Run Code Online (Sandbox Code Playgroud)

之前的%include简单案例。

这样做的主要内容是:

  1. 更改中间函数以通过引用接受 IntPtr 以供输出。这最终将保存我们想要传递给make它的值,它本身就是一个指向 a 的指针std::shared_ptr
  2. 对于 csin 类型映射,我们将安排创建一个临时 IntPtr 并将其用于中间调用。中间调用发生后,我们需要将输出传递make给并将结果IBase实例分配给我们的输出参数。
  3. 当我们调用真正的 C++ 函数时,我们需要构造一个 shared_ptr 以便在调用时绑定到引用。我们还将 shared_ptr 的地址存储到我们的输出参数中,以便 C# 代码可以选择它并在以后使用它。

现在这足以解决我们的问题。我在原始测试用例中添加了以下代码:

    IBase result2;
    test.factory2(DataTypes.Float64, out result2);
    Console.WriteLine(result2.GetType().FullName);
Run Code Online (Sandbox Code Playgroud)

提醒一句:这是我写过的最大的 C# 代码块。我使用 Mono 在 Linux 上测试了所有内容:

%module test

%{
#include "test.h"
%}

%include <std_shared_ptr.i>

%shared_ptr(IBase) 
%shared_ptr(CDerived<float>)
%shared_ptr(CDerived<double>)
%shared_ptr(CDerived<int>)

%newobject factory; // 1

%typemap(csout, excode=SWIGEXCODE) IBasePtr { // 2
    IntPtr cPtr = $imcall;
    var ret = $imclassname.make(cPtr, $owner);$excode // 3
    return ret;
}

%include "test.h" // 4

%template (CDerivedFloat) CDerived<float>;
%template (CDerivedDouble) CDerived<double>;
%template (CDerivedInt) CDerived<int>;

%pragma(csharp) imclasscode=%{
    public static IBase make(IntPtr cPtr, bool owner) {
        IBase ret = null;
        if (IntPtr.Zero == cPtr) return ret;

        ret = new IBase(cPtr, false); // 5
        switch(ret.DataType()) {
            case DataTypes.Float32:
                ret = new CDerivedFloat(cPtr, owner);
                break;
            case DataTypes.Float64:
                ret = new CDerivedDouble(cPtr, owner);
                break;
            case DataTypes.Integer32:
                ret = new CDerivedInt(cPtr, owner);
                break;
            default:
                if (owner) ret = new IBase(cPtr, owner); // 6
                break;
        };
        return ret;
    }
%}
Run Code Online (Sandbox Code Playgroud)

运行时给出:

inline bool factory2(const DataTypes type, IBasePtr& result) {
    try {
        result = factory(type);
        return true;
    }
    catch (...) {
        return false;
    }
}
Run Code Online (Sandbox Code Playgroud)

我认为生成的编组是正确的,但您应该自己验证。