构建dll的正确方法

Right way to build dll

我们有一个 class A(在下面声明)。

#ifdef DLL_ON_WORK
    #define DLL_SPEC __declspec(dllexport)
#else
    #define DLL_SEPC __declspec(dllimport)

class A
{
    friend B;
private:
    int a;
    int b;
    vector<int> c;

public:
    DLL_SPEC int Geta();
    DLL_SPEC int Getb();
    DLL_SPEC vector<int> Getc();
             int Calc();

private:
    void Seta(int val);
    void Setb(int val);
    void Setc(vector<int>& val);
};

我有几个问题。

  1. 如果classA在客户端代码中动态和静态创建和删除,是否必须将整个classA标记为DLL_SPEC?
  2. 需要我为客户端代码创建 "special" 版本的头文件,比如 SDK,其中将删除客户端代码方法和字段的所有私有和不可用?我可以这样做吗?
  3. 如果要在客户端代码中使用,实际头文件应该提供什么,完整的 class 声明,或者我只能指定 class 客户端应该使用的接口?

我知道,这些问题相互交叉,但我在构建库及其在客户端代码中的进一步使用方面存在一些知识空白,如果你能推荐一些关于这个主题的书籍或文章,那将是很好的.

我会在你的情况下使用 Factory-Pattern(老实说,我总是使用工厂模式)。

那样的话,你只会导出两个函数

DLL_SPEC A *createClassA();
DLL_SPEC void destroyClassA(A *obj);

对于其余部分,您需要任何说明符 virtual,这使得思考更容易处理。

优势:

  • 您可以将 header 文件分成两个(工厂定义和通常定义)。您的 class 的定义对于用户和 dll 都是相同的。
  • 每个方法前面都没有macro-modifier。

缺点:

  • 您只能通过工厂函数创建和销毁 object。 DLL 内存边界将阻止在 dll 中使用 new 并在主代码中使用 delete。但这并不是真正的问题。如果你使用 std::shared_ptr 你可以提供一个特殊的销毁函数。

您还可以将工厂函数作为 static 方法放入 class 中。在这种情况下,您可以将构造函数和析构函数声明为私有,这可以防止您的 dll 用户在没有工厂的情况下创建 A:

#ifdef DLL_ON_WORK
    #define DLL_SPEC __declspec(dllexport)
#else
    #define DLL_SEPC __declspec(dllimport)

class A
{
    friend B;
private:
    int a;
    int b;
    vector<int> c;

public:
    virtual int Geta();
    virtual int Getb();
    virtual vector<int> Getc();
    virtual int Calc();
    static DLL_SPEC A *createClassA();
    static DLL_SPEC void destroyClassA(A *obj);
private:
    virtual void Seta(int val);
    virtual void Setb(int val);
    virtual void Setc(vector<int>& val);
};

最后但并非最不重要的一点。与 interface-pattern 一起工作。只导出接口,不导出 class 本身。将您的 class 从 'A' 命名为 'Aimplementation',它继承自将导出的接口 'IA'。界面将与您的情况类似:

#ifdef DLL_ON_WORK
    #define DLL_SPEC __declspec(dllexport)
#else
    #define DLL_SEPC __declspec(dllimport)

class IA
{
public:
    virtual int Geta()=0; // with C++11 please use =nullptr instead of
    virtual int Getb()=0; 
    virtual vector<int> Getc()=0;
    virtual int Calc();


    static DLL_SPEC IA *createA();
    static DLL_SPEC void destroyA(IA *obj);
};

恕我直言,这是最巧妙的方法,也是您三个问题的答案。

更新

我之前忘记了 virtual 关键字,这对于此解决方案是必需的。关键字 virtual 强制编译器生成带有 entry-points 的 vtable 到使用 new.

创建的每个 object 的方法

DLL 中工厂方法的典型实现如下所示:

IA* IA::createA()
{
 return new Aimplemetation;
}

void IA::destroyA(IA *obj)
{
  delete static_cast<Aimplementation *>(obj);
}

要在 DLL 用户的代码中调用 Geta,主要代码如下:

void main()
{
 IA *a=IA::createIA();
 a->Geta();
 IA::destroyIA(a);
}

@Christophe 在评论区总结如下:

So the trick is that on the client side, the compiler deduces from the header's class definition the vtable layout of the object. He then generates the call using the vtable indirection without need to expose the function name ! and of course using the same calling conventions.

通常每个库我只导出两个函数。一个 create/init 和 destroy/close。通过返回 class 的第一个实例,我或多或少地暴露了整个界面。在我看来,这也使代码更具可读性。这种模式也可以用于 plugin-systems,其中客户端(例如 Chrome)提供 header 并且 plugin-DLL 必须满足接口。