【问题标题】:How does passing objects and calling member functions between libraries work?库之间传递对象和调用成员函数是如何工作的?
【发布时间】:2016-10-03 09:20:03
【问题描述】:

我试图了解当我将核心应用程序的类文件包含到库(Qt-Plugin)的编译中时会发生什么。假设我有一个插件 - 一个处理程序 - 和一个 Query(h,cpp) (带有私有实现) - 要处理的对象。

编辑

query.h(来自链接)

class Query final
{
public:
    friend class ExtensionManager;

    Query(const QString &term);
    ~Query();
    void addMatch(shared_ptr<AlbertItem> item, short score = 0);
    void reset();
    void setValid(bool b = true);
    bool isValid();
private:
    QueryPrivate *impl;
};

我假设编译器,至少在链接阶段,获取目标文件并将其放入共享目标文件中。但实际上名称查询并没有出现在cmake编译和链接过程的输出中(本质上是执行的g++命令),只是它的目录的includes。

当我编译插件/库时,编译器/链接器除了检查接口/标头之外,还会做其他事情吗?插件如何在运行时知道有关查询的任何信息?对象的插件调用是如何在运行时起作用的?

【问题讨论】:

  • 输入赏金信息时触摸板失败...
  • Qt 使用 PIMPL 将公共头文件与实现分开,因此调用者只需要将头文件链接到库。

标签: c++ qt gcc


【解决方案1】:

插件如何在运行时知道查询?

在不同的编译单元(dll、共享对象、可执行文件)之间共享信息是一个有问题的设计。

  • C++ ABI 没有标准。这允许不同的编译器提供者来布局他们的对象(例如,vtable 在哪里,虚拟析构函数在 vtable 中的哪里),以及如何在同一台机器上以不同的方式调用方法。
  • .h 文件是一个弱接口定义,并且会遇到#define 在同一事物的不同编译器之间可能不同的问题。 (例如,Microsoft 调试 STL 不适用于发行版 STL)。
  • .h 中存储的内联函数可能会导致在库和插件之间调用不同的实现。
  • 内存管理可能会受到影响,因为释放对象的代码可能不了解它的分配位置和方式。

修改数据

假设一个类有公共成员,(并且两个模块共享一个编译器)这些可以在创建对象的库和实现它的库中修改。

class Example1 {
    public:
      int value1;
};

在可执行文件中。

example1.value1 = 12;

在插件中

if( this->value1 == 12 ){
}

这不适用于复杂的对象,例如std::string.

调用函数

class Example2 {
      public:
      void AFunction();
};

AFunction 的任何调用者都需要一个可用的实现。这将被静态调用,并且可以在二进制文件和共享对象之间共享

 +-------------------+          +-----------------------+
 | binary            |          | shared object         |
 | Query::AFunction()|          |                       |
 | {                 |          |  Process( Query &q )  |
 | }                 |          |  {                    |
 |                   |    o-->  |     q.AFunction();    | <<< may be in
 | main()            |    |     |                       | shared object
 | {                 |    |     |                       | could call binary
 |    Query q;       |    |     |                       |
 |    Process( q );  | ===o     |                       |
 +-------------------+          +-----------------------+

如果共享对象有一个实现(它是一个内联函数,或者 query.cpp 包含在共享对象makefile 中),那么AFunction 的实现可能是不同的。

**使用 STL - 两个二进制文件都有自己的实现,如果它们在不同时间编译,可能会不同(并且不兼容)。 **

共享对象的行为是这样的,如果它有未解析的外部,加载它的二进制文件满足,它将使用它们的实现。在 windows 上不是这样,可以使用-z, defs 生成 windows 行为。

为了调用非虚函数,调用者需要在编译时知道类。该方法是一个固定调用,第一个(通常)参数是 this 指针。因此,为了生成代码,编译器直接(或通过修复表)调用函数。

调用虚函数

虚拟函数总是通过 this 指针调用,这意味着类的虚拟函数是由构造对象的代码“选择”的。这在 Windows 中用于 COM 实现,是一种有用的对象共享技术 - 允许在框架编译后交付具有不同功能的新类,但无需任何知识即可调用实现对象。

vtable 需要稳定才能正常工作。在编译调用者和被调用者以使这一切正常工作时,基类或接口应该相同。

在设计库时,可以生成接口对象。

class ICallback {
     virtual void Funcion1( class MyData * data ) = 0;
};

编译库时,它不知道实现 ICallback 及其任何函数,但它知道如何调用这些函数。

所以一个函数定义

class Plugin {
     bool Process( ICallback * pCallback );
};

允许在不知道回调实现的情况下声明和实现函数 (ICallback)。这不会创建未解析的符号,也不需要插件在编译插件之前知道该项目。它所需要的只是它的调用者(m_pluginObject.Process( &amp;myQueryImplementation );)有一个创建的具体类型来传递。

编译

当编译器编译代码时,它会创建一个目标文件(.obj 用于 windows,.o 用于 unix)。

在这个文件中,是链接文件所需的所有代码和数据定义。

名义目标文件

<dictionary>
    int SomeIntValue = Address1
    bool Class1::SomeFunction( char * value ) = Address2
</dictionary>
<Requires>
    std::ostream::operator<<( const char *);
    std::cout
</Requires>
<Data>
      Address1 : SomeIntValue = 12
</Data>
<Code>
    Address2 .MangledSomeFunctionCharStarBool
                  // some assembly
          call ostream::operator<<(char*)
</Code>

这个 objecf 文件中应该有足够的信息来满足编译过程的一部分。虽然通常像MyClass.cc 这样的文件可能具有实现MyClass 所需的所有功能,但它不需要具有所有这些功能。

当编译器读取头文件或任何类声明时,它会创建一个未解析的外部列表,稍后将需要该列表。

 class Class1 {
       int ClassData;
    public:
        bool SomeFunction( char * value);
        ....
 };

描述有一个 Class1 的成员函数接受 char * 作为值,并且返回值将是 bool。在继续编译C++程序时,编译器看到如

  bool Class1::SomeFunction( char * value )
  {
     bool success = false;
     cout << value;
       // some work
      return success;
  }  

这个实现的功能被添加到实现什么的字典中,它需要的功能和数据被添加到需求中。

库文件

库文件在 unix 和 windows 上略有不同。最初,unix 库文件是 .o 文件的容器。这些只是 .o 的串联项目 (ar)。然后为了找到正确的项目,图书馆被索引(ranlib)以产生一个工作图书馆。最近,我相信档案的标准已经改变,但概念必须保持不变。

链接库

在 windows 中,在构建 DLL 时会创建一个链接库,在 unix 中,链接库是在共享对象中构建的。

链接库是动态加载对象的可交付成果列表以及交付它的.dll、.so 的名称。这会导致将信息添加到二进制文件中,例如:-

<SharedObjects>
     printf : glibc:4.xx
</SharedObjects>

描述需要加载的共享对象,以及它们提供的功能(该程序的子集)。

链接

当编译器生成二进制文件(.so、.dll、.exe 或 unix 二进制文件)时,命令行上指定的目标文件将绑定到二进制文件中。这会创建一组已实现的功能(例如main)和一组未解决的需求。

然后搜索每个库(.a、.lib)以查看它们是否提供了完成完整流程所需的功能。如果他们确实提供了任何功能,那么这将被视为已解决。实现解析功能的单个目标文件被完全添加到二进制文件中。

他们也可能有要求,这些是:-

  1. 已由已加载的二进制文件解决
  2. 添加到未解析的值。

请注意,库的顺序很重要,因为只有所需的部分库会添加到二进制文件中。

如果此过程成功,则在 windows 上,所有需要的功能都已添加。

在 unix 上,您可能需要传递 -z,defs SO : unresolved externals。这允许 unix .so 的某些要求由加载的二进制文件来满足,但可能会导致二进制文件不完整。

总结

二进制文件有:-

  1. 链接命令行中的所有目标文件。
  2. 满足未解析的外部要求的静态库中的任何目标文件
  3. shared objects 的列表及其交付工作程序所需的功能。
  4. 使用接口和基类,允许在原始设计完成后添加新类。

【讨论】:

  • OP 似乎确实有一个可以工作的插件系统,而 Query 不是一个接口。我想这个问题的一个方面是插件如何知道正确调用函数(从 exe 加载时加载符号,已经使用其自己的 Query 的 impl 版本编译,使用 exe 中的目标文件进行编译。 ..?)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-12-29
  • 1970-01-01
  • 2017-03-17
  • 2013-12-26
  • 2012-03-07
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多