1

我正在用 C++ 创建一个 DLL。这是一个例子:

namespace MathFuncs
{
  class MyMathFuncs
  {
    public:
        // Returns a + b
        static __declspec(dllexport) double Add(double a, double b);

        // Returns a - b
        static __declspec(dllexport) double Subtract(double a, double b);

        // Returns a * b
        static __declspec(dllexport) double Multiply(double a, double b);

        // Returns a / b
        // Throws DivideByZeroException if b is 0
        static __declspec(dllexport) double Divide(double a, double b);
    };
}

所有方法都是静态的,静态方法有很多限制,所以我的问题是如何在没有静态方法的情况下实现相同的方法?我总是需要在 DLL 中使用静态方法吗?我想在 C# 和 IOS 应用程序中导入这个 DLL。

4

2 回答 2

5

您必须使用全局的 C 风格方法。其原因在此处描述

基本上可以归结为:C 函数很好地转换为 DLL 导出,因为 C 在语言特性方面“更接近地面”。C 更直接地翻译成机器代码。C++ 在编译器级别做了很多工作,为您提供了许多不能在 C++ 环境之外使用的功能。出于这个原因,您的导出函数应遵循 C 风格,以便跨 DLL 边界正常运行。这意味着没有模板、没有内联代码、没有非 POD 类或结构。

考虑这段代码:

extern "C"
{
    __declspec(dllexport) int GlobalFunc(int n)
    {
        return n;
    }

    namespace SomeNamespace
    {
        __declspec(dllexport) int NamespaceFunction(int n)
        {
            return n;
        }
    }

    class MyClass
    {
        __declspec(dllexport) int ClassNonStatic(int n)
        {
            return n;
        }
        __declspec(dllexport) static int ClassStatic(int n)
        {
            return n;
        }
    };

}

这将导致以下 DLL 导出函数名称:

?ClassNonStatic@MyClass@@AAEHH@Z

?ClassStatic@MyClass@@CAHH@Z

全球函数

命名空间函数

除了 Visual Studio 构建的 C++ 项目之外,具有有趣命名的项目基本上与任何其他项目都不兼容。这称为name mangling,将一些类型信息嵌入到名称本身中,以解决我正在谈论的导出函数的限制。从技术上讲,您可以在外部使用这些函数,但它很脆弱,并且依赖于编译器特定行为的细微差别。

在 DLL 中导出函数的经验法则是:你能在 C 中做到这一点吗?如果你不能,那么几乎可以肯定你会引起问题。

请注意,即使是静态类方法(本质上是全局的),即使使用extern "C". 但是命名空间导出中的独立函数没有名称修改(尽管它们失去了命名空间范围)。

你可以开始明白为什么这个经验法则是有意义的。


如果你想导出一个类,让我们按照经验法则,像在 C 中那样设计 DLL 接口。这里有一个例子。让我们来看看这个 C++ 类:

    class Employee
    {
    private:
        std::string firstName;
        std::string lastName;

    public:
        void SetFirstName(std::string& s)
        {
            this->firstName = s;
        }
        void SetLastName(std::string& s)
        {
            this->lastName = s;
        }
        std::string GetFullName()
        {
            return this->firstName + " " + this->lastName;
        }
    };

你不能只坚持__declspec(dllexport)这一点。您必须为其提供一个 C 接口,并将其导出。像这样:

    extern "C"
    {
        __declspec(dllexport) Employee* employee_Construct()
        {
            return new Employee();
        }

        __declspec(dllexport) void employee_Free(Employee* e)
        {
            delete e;
        }

        __declspec(dllexport) void employee_SetFirstName(Employee* e, char* s)
        {
            e->SetFirstName(std::string(s));
        }

        __declspec(dllexport) void employee_SetLastName(Employee* e, char* s)
        {
            e->SetLastName(std::string(s));
        }

        __declspec(dllexport) int employee_GetFullName(Employee* e, char* buffer, int bufferLen)
        {
            std::string fullName = e->GetFullName();
            if(buffer != 0)
                strncpy(buffer, fullName.c_str(), bufferLen);
            return fullName.length();
        }
    }

然后在 C# 端再写一个小包装器,你就成功地封送了这个类。

专门针对 C# 编组,另一种选择是为您的类提供 COM 接口而不是 C 接口。本质上是一样的,但是有很多帮助类和特殊的编译器支持直接将 COM 支持添加到 C++ 类,而无需编写单独的包装器。COM 对象可以被 C# 直接引用。

不过,这对您的ios没有帮助...

于 2012-09-07T08:16:26.680 回答
1

作为旁注,我几天前用 mingw/c++ 做了 1 个实验,这可以为你澄清。

我有一个全局引用计数器,用于找出程序中的内存泄漏,

class ReferenceCounter /** other implementations details are omitted.*/
{
public:

static int GlobalReferenceCounter;

//version 1
static int getReferenceCount1() { return GlobalReferenceCounter;}

//verison 2
static int getReferenceCount2(); //same code of version 1 but moved into .cpp file
};

当使用引用计数器将我的库编译到 DLL 中时,变量被复制,1 个版本被编译到 DLL 中,一个版本被编译到客户端代码中。

当我从 DLL 的工厂方法中询问引用计数的东西时,只有 DLL 内的引用计数器被增加/减少。当客户端代码使用从 Ref Counter 继承的自己的类时,客户端引用计数器会增加/减少。

因此,为了检查内存泄漏,我必须在程序结束时进行

assert(ReferenceCounter.getReferenceCount1() == 0);
assert(ReferenceCoutner.getReferenceCount2() == 0);

这是因为如果发生内存泄漏,其中一个值将大于 0。如果第一个值大于 1,则内存泄漏是由未分配的用户类引起的,如果第二个值大于 0,则内存泄漏是由库引起的类。

请注意,如果泄漏是由未分配的库的类引起的,这不一定是库的错误,因为用户仍然能够泄漏该类,即使这意味着库中缺乏设计,因为理想情况下应该返回所有内容在适当的智能指针中以确保安全。)

当然,您应该在文档中指定“GlobalReferenceCoutner”是重复的,否则一些不知情的用户可能会认为 2 个 getter 是多余的,并且会认为您犯了一些错误。(如果可能的话,避免做这样的事情,是晦涩难懂的)

这也应该警告您,通过 DLL 访问静态方法是非常不安全的。例如,如果在我的课堂上我只想有 1 个引用计数器而不是 2,我必须这样做以确保授予安全:

class ReferenceCounter
{
public:

static int GlobalReferenceCounter;

static void duplicate() { increaseGlobalCounter(); }

static void release() { decreaseGlobalCounter(); }

static int getGlobalCounter() { return privateGetGlobalCounter(); }

private:

static increaseGlobalCounter(); // implementation into Cpp file

static decreaseGlobalCounter(); // implementation into Cpp file

static privateGetGlobalCounter(); // implementation into Cpp file

};

这样做将使您在 DLL 边界和用户应用程序中使用相同的值。因此,我们在这里使用 1 个变量,而不是使用 2 个不同的变量(可能 GlobalCounter 仍被编译为用户可执行文件,但没有人使用它来消除“克隆效应”)

于 2012-09-07T08:48:50.220 回答