使用 %typemap 的 SWIG 和多态 C# [英] SWIG and polymorphic C# using %typemap

查看:61
本文介绍了使用 %typemap 的 SWIG 和多态 C#的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I would like to support downcasting in a SWIG-generated C# project.

我有一系列 C++ std::shared_ptr 包装的类模板,它们继承自一个公共基础.任何在 C++ 代码中返回基类 (IBasePtr) 的 C++ 方法都会导致生成的方法返回一个具体的 IBase 对象,该对象与我实际上是的对象无关试图得到.博客文章 here 通过插入自定义代码来执行基于向下转换的对象类型元数据.

I have a series of C++ std::shared_ptr-wrapped class templates that inherit from a common base. Any C++ method that returns a base class (IBasePtr) in C++ code results in a generated method that returns a concrete IBase object, which has no relation to the object I am actually trying to get. The blog post here deals with this exact problem by inserting custom code to perform a downcast based on object type metadata.

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

C++ (simplified for the purpose of illustration):

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:

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 已生成,但没有对其的引用.

The part I am struggling with is the use of SWIG's %typemap command. %typemap is intended to instruct SWIG to map input and target types, in my case via the code to perform an explicit conversion. The method InstantiateConcreteClass is generated but there are no references to it.

是否有我遗漏的重要步骤?我想知道这是否是由于在本机代码中使用 shared_ptr 而造成的一些额外的复杂性,但我不认为是这种情况.

Is there a vital step I am missing? I wondered whether the was some additional complication due to the use of shared_ptr in native code, but I don't think this is the case.

推荐答案

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

The problem with your example seems to be that you've written typemaps for inputs, but that doesn't seem to make sense in and of itself, because the important part is getting the type right when creating things, not using them as input. As far as output arguments go the second half of this answer addresses that, but there are errors with using your typemaps for arguments too.

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

I've simplified your example fractionally and made it complete and working. The main thing I had to add that was missing was a 'factory' function that creates derived instances, but returns them as the base type. (If you just create them with new directly then this isn't needed).

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

I merged your header files and implemented an inline factory as 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 仅从 T 中查找 DataType 的正确值模板参数并将 DataType 更改为常量.我这样做是因为让 CDerived 实例对它们的类​​型撒谎是没有意义的 - 它只设置一次,不应该进一步公开.

The main changes here being the addition of some template meta programming to allow IBase to lookup the correct value of DataType from just the T template parameter and changing DataType to be const. I did this because it doesn't make sense to let CDerived instances to lie about their type - it's set exactly once and isn't something that should be exposed any further.

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

Given this I can then write some C# that shows how I intend to use it after wrapping:

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

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

Essentially if my typemaps are working we will have used the DataType member to transparently make test.factory return a C# proxy that matches the derived C++ type rather than a proxy that knows nothing more than the base type.

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

Note also that here because we have the factory we could also have modified the wrapping of that to use the input arguments to determine the output type, but this is less generic than using the DataType on the output. (For the factory approach we'd have to write per-function rather than per-type code for the correct wrapping).

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

We can write a SWIG interface for this example, which is substantially similar to yours and the blog post referenced but with a few changes:

%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 个显着变化:

There are 6 notable changes highlighted via comments in that typemap:

  1. 我们告诉 SWIG,factory 返回的对象是新的,即所有权从 C++ 转移到 C#.(这会导致 owner 布尔值正确设置)
  2. 我的 typemap 是一个 csout typemap,这是唯一需要的.
  3. 与您链接的教程相比,我使用了 $imclassname,它始终正确地扩展为 $modulePINVOKE 或等效物.
  4. 我直接将 %include 与我的头文件一起使用,以避免不必要地重复我自己.
  5. 我没有接触包装器的内部工作原理,而是直接创建了一个 IBase 的临时实例,它允许我以更简洁的方式访问枚举值.临时实例的所有权设置为 false,这意味着我们在处理它时永远不会错误地删除底层 C++ 实例.
  6. 如果由于某种原因无法找出派生类型,我选择让默认路径通过 switch 语句返回一个 IBase 实例,而不知道派生类型.
  1. We've told SWIG that the objects returned by factory are new, i.e. there is a transfer of ownership from C++ to C#. (This causes the owner boolean to get set correctly)
  2. My typemap is a csout typemap and that's the only one needed.
  3. Compared to the tutorial you linked I used $imclassname, which expands to $modulePINVOKE or equivalent correctly always.
  4. I used %include with my header file directly to avoid repeating myself lots unnecessarily.
  5. Rather than touch the inner workings of the wrapper I created a temporary instance of IBase directly that allowed me to access the enum value in a much cleaner way. The temporary instance has ownership set false that means we never incorrectly delete the underlying C++ instance when disposing of it.
  6. I chose to let the default path through the switch statement return an IBase instance with no knowledge of the derived type if it couldn't be figured out for some reason.

<小时>

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


Based on what you showed in your question it actually looks like what you're mostly struggling with is output reference arguments. Without the shared_ptr angle this wouldn't work at all. The simplest solution for wrapping this is to use %inline or %extend within SWIG to write an alternative version of the function to be used that doesn't pass output via reference arguments.

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

We can however make this work naturally on the C# side too, with some more typemaps. You're on the right track with the OUTPUT and %apply style typemaps you've shown, however I don't think you've got them quite right. I've extended my example to cover this as well.

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

First, although I don't much like using functions like this I added factory2 to test.h:

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

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

The key thing to note here is that by the time we call factory2 we must have a valid reference to an IBasePtr (std::shared_ptr<IBase>), even if that shared_ptr is null. Since you're using out instead of ref in you're C# we'll need to arrange to make a temporary C++ std::shared_ptr before the call actually happens. Once the call happens we want to pass this back to the make static function we wrote for the simpler case earlier.

我们将不得不仔细研究 SWIG 如何处理智能指针以实现这一切.

We're going to have to look fairly closely at how SWIG handles smart pointers to make this all work.

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

So secondly my SWIG interface ended up adding:

%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 之前.

Before the %include of the simple case.

主要作用是:

  1. 更改中间函数以通过引用接受 IntPtr 用于输出.这最终将保存我们想要传递给 make 的值,它本身就是一个指向 std::shared_ptr 的指针.
  2. 对于 csin 类型映射,我们将安排创建一个临时 IntPtr 并将其用于中间调用.中间调用发生后,我们需要将输出传递给 make 并将结果 IBase 实例分配给我们的输出参数.
  3. 当我们调用真正的 C++ 函数时,我们需要构造一个 shared_ptr 以便在调用时绑定到引用.我们还将 shared_ptr 的地址存储到我们的输出参数中,以便 C# 代码可以选择它并在以后使用它.
  1. Change the intermediate function to accept an IntPtr by reference for output. This will eventually hold the value that we want to pass in to make that is itself a pointer to a std::shared_ptr.
  2. For the csin typemap we're going to arrange to create a temporary IntPtr and use that for the intermediate call. After the intermediate call has happened we then need to pass the output into make and assign the resulting IBase instance to our output parameter.
  3. When we call the real C++ function we need to construct a shared_ptr for it to bind to the reference when we make the call. We also store the address of the shared_ptr into our output parameter so that C# code can pick it up and work with it later.

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

This is now sufficient to solve our problem. I added the following code to my original test case:

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

<小时>

请注意:这是我编写过的最大的 C# 代码块.我使用 Mono 在 Linux 上测试了所有内容:


A word of caution: this is the biggest chunk of C# code I've ever written. I tested it all on Linux with Mono using:

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

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

and I think the generated marshalling is correct but you should verify it yourself.

这篇关于使用 %typemap 的 SWIG 和多态 C#的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆