SWIG 和使用 %typemap 的多态 C#

2024-02-18

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

我有一个C++系列std::shared_ptr- 从公共基础继承的包装类模板。任何返回基类的 C++ 方法 (IBasePtr) 在 C++ 代码中会生成一个返回具体值的生成方法IBase对象,它与我实际想要获取的对象无关。博客文章here http://johnnado.com/swig-csharp-java-downcast/通过插入自定义代码来根据对象类型元数据执行向下转换来解决这个确切的问题。

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;
    };
}

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;
    };
}

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;
    };
}

斯威格接口:

%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;
    }
%}

我遇到的困难是使用 SWIG%typemap命令。%typemap旨在指示 SWIG 映射输入和目标类型,在我的例子中通过代码执行显式转换。生成了 InstantiateConcreteClass 方法,但没有对其的引用。

我是否缺少重要的一步?我想知道是否由于使用了一些额外的并发症shared_ptr在本机代码中,但我认为情况并非如此。


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

我已经稍微简化了您的示例,并使其完整且有效。我必须添加的主要内容是缺少一个“工厂”函数,该函数创建派生实例,但将它们作为基本类型返回。 (如果你只是用创建它们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();
}

这里的主要变化是添加了一些模板元编程,以允许IBase查找正确的值DataType仅从T模板参数及更改DataType成为常量。我这样做是因为让CDerived实例谎报其类型 - 它只设置一次,并且不应该进一步暴露。

鉴于此,我可以编写一些 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);
    }
}

本质上,如果我的类型映射正常工作,我们将使用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;
    }
%}

该类型映射中通过注释突出显示了 6 个显着变化:

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

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

然而,我们也可以通过更多的类型映射在 C# 端自然地实现这一点。您的输出方向是正确的,并且%apply您已经展示了样式类型映射,但我认为您的理解不太正确。我也扩展了我的示例来涵盖这一点。

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

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

这里要注意的关键是当我们调用时factory2我们必须有一个有效的引用IBasePtr (std::shared_ptr<IBase>),即使该shared_ptr为空。既然你正在使用out代替ref如果你是 C#,我们需要安排一个临时的 C++std::shared_ptr在通话实际发生之前。一旦调用发生,我们想将其传回给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 }

之前%include的简单情况。

它所做的主要事情是:

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

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

    IBase result2;
    test.factory2(DataTypes.Float64, out result2);
    Console.WriteLine(result2.GetType().FullName);

需要注意的是:这是我编写过的最大的 C# 代码。我在 Linux 上使用 Mono 测试了所有这些:

swig -c++ -Wall -csharp test.i && mcs -g hello.cs *.cs && g++ -std=c++11 -Wall -Wextra -shared -o libtest.so test_wrap.cxx
warning CS8029: Compatibility: Use -debug option instead of -g or --debug
warning CS2002: Source file `hello.cs' specified multiple times

运行时给出:

CDerivedFloat
CDerivedInt
CDerivedDouble

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

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SWIG 和使用 %typemap 的多态 C# 的相关文章

随机推荐