您的示例的问题似乎是您已经为输入编写了类型映射,但这本身似乎没有意义,因为重要的部分是在创建事物时获得正确的类型,而不是使用它们作为输入。就输出参数而言,这个答案的后半部分解决了这个问题,但是使用类型映射作为参数也存在错误。
我已经稍微简化了您的示例,并使其完整且有效。我必须添加的主要内容是缺少一个“工厂”函数,该函数创建派生实例,但将它们作为基本类型返回。 (如果你只是用创建它们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 个显着变化:
- 我们已经告诉 SWIG 返回的对象
factory
是新的,即所有权从 C++ 转移到 C#。 (这会导致owner
布尔值以正确设置)
- 我的类型映射是 csout 类型映射,这是唯一需要的类型映射。
- 与我使用的您链接的教程相比
$imclassname
,扩展到$modulePINVOKE
或始终正确地等效。
- I used
%include
直接使用我的头文件以避免不必要的重复。
- 我没有接触包装器的内部工作原理,而是创建了一个临时实例
IBase
直接允许我以更简洁的方式访问枚举值。临时实例的所有权设置为 false,这意味着我们永远不会错误地delete
处理它时的底层 C++ 实例。
- 我选择让默认路径通过 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
的简单情况。
它所做的主要事情是:
- 更改中间函数以通过引用接受 IntPtr 进行输出。这最终将保存我们想要传递给的值
make
它本身就是一个指向std::shared_ptr
.
- 对于 csin 类型映射,我们将安排创建一个临时 IntPtr 并将其用于中间调用。中间调用发生后,我们需要将输出传递到
make
并分配结果IBase
我们的输出参数的实例。
- 当我们调用真正的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
我认为生成的编组是正确的,但您应该自己验证。