SWIG和多态C#使用%typemap

时间:2014-11-27 16:44:41

标签: c# swig

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

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

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

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

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

我缺少一个至关重要的步骤吗?我想知道由于在本机代码中使用shared_ptr是否是一些额外的复杂性,但我不认为是这种情况。

2 个答案:

答案 0 :(得分:4)

您的示例的问题似乎是您已经为输入编写了类型地图,但这似乎并没有任何意义,因为重要的部分是在创建内容时使类型正确,而不是使用它们作为输入。至于输出参数,这个答案的后半部分解决了这个问题,但是也有使用你的参数类型图的错误。

我已经简化了您的示例并使其完整且有效。我不得不补充的主要内容是工厂&#39;创建派生实例的函数,但将它们作为基类型返回。 (如果您只是直接使用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是const。我之所以这样做,是因为让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;
    }
%}

通过该typemap中的注释突出显示了6个值得注意的更改:

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

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

    然而,我们可以在C#方面自然地使用更多的类型映射。您已经使用OUTPUT和%apply样式输入显示在正确的轨道上,但我不认为您已经将它们设置得恰到好处。我已经扩展了我的例子以涵盖这一点。

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

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

    这里要注意的关键是,当我们调用factory2时,我们必须拥有对IBasePtrstd::shared_ptr<IBase>)的有效引用,即使shared_ptr为null。由于您在C#中使用out而不是ref,因此我们需要安排在呼叫实际发生之前制作临时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本身就是指向make的指针。
    2. 对于csin typemap,我们将安排创建一个临时IntPtr并将其用于中间调用。在中间调用发生后,我们需要将输出传递给IBase并将结果 IBase result2; test.factory2(DataTypes.Float64, out result2); Console.WriteLine(result2.GetType().FullName); 实例分配给输出参数。
    3. 当我们调用真正的C ++函数时,我们需要构造一个shared_ptr,以便在我们进行调用时绑定到引用。我们还将shared_ptr的地址存储到我们的输出参数中,以便C#代码可以将其拾取并在以后使用它。
    4. 这足以解决我们的问题。我将以下代码添加到原始测试用例中:

      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
      

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

      CDerivedFloat
      CDerivedInt
      CDerivedDouble
      

      运行时给出了:

      {{1}}

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

答案 1 :(得分:1)

在上面的Flexo示例的帮助下完成了这项工作。在这里使用%newobject至关重要 - 在我的原始问题中,派生对象的生命周期未得到正确管理。

我需要做一个小改动 - 需要将命名空间名称添加到typemap中:

%typemap(csout, excode=SWIGEXCODE) MyLib::IBasePtr { // Need the fully-qualified name incl. namespace
    IntPtr cPtr = $imcall;
    var ret = $imclassname.make(cPtr, $owner);$excode // 3
    return ret;
}

在第6点之后没有必要进行更改。