当我将核心应用程序的类文件包含在库的编译(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 ++命令),只是其目录的包含。
当我编译插件/库时,编译器/链接器除了检查接口/头之外还做了什么吗?插件如何在运行时了解有关Query的任何信息?插入调用如何在运行时对对象起作用?
答案 0 :(得分:3)
插件如何在运行时了解查询?
在不同的编译单元(dll,共享对象,可执行文件)之间共享信息是一个有问题的设计。
.h
文件是一个弱接口定义,并且#define
可能在同一事物的不同编译器之间存在差异。 (例如,Microsoft调试STL不适用于发行版STL)。假设一个类有公共成员,(并且两个模块共享一个编译器),可以在创建该对象的库和实现它的库中修改它们。
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
的实现可能是不同的。 / p>
**使用STL - 两个二进制文件都有自己的实现,如果它们在不同时间编译,可能会有所不同(并且不兼容)。 **
共享对象的行为是这样的,如果它有未解析的外部,它正在加载它的二进制文件,它将使用它们的实现。在Windows上不是这样,可以使用-z, defs
生成窗口行为。
为了调用非虚函数,调用者需要在编译时知道该类。该方法是固定调用,第一个(通常)参数是this指针。因此,为了生成代码,编译器直接(或通过修复表)调用函数。
虚拟函数总是通过this指针调用,这意味着选择了一个类的虚函数&#39;通过构造对象的代码。这在Windows中用于COM实现,并且是一种用于对象共享的有用技术 - 允许在框架编译之后提供具有不同功能的新类,但没有任何知识调用实现对象。
vtable需要稳定才能工作。编译调用者和被调用者时,基类或接口应该是相同的。
设计库时,可以生成一个接口对象。
class ICallback {
virtual void Funcion1( class MyData * data ) = 0;
};
编译库时,它不知道实现 ICallback及其任何功能,但它确实知道如何调用它们。
所以函数定义
class Plugin {
bool Process( ICallback * pCallback );
};
允许声明和实现函数,而不知道回调的实现(ICallback
)。这不会创建一个未解析的符号,也不需要插件在编译插件之前知道该项目。它需要的是,它的调用者(m_pluginObject.Process( &myQueryImplementation );
)具有一个可以传入的具体类型。
当编译器编译代码时,它会为windows创建一个目标文件(.obj
,在unix上创建.o
)。
在此文件中,是链接文件所需的所有代码和数据定义。
<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
)以查看它们是否提供完成整个过程所需的功能。如果他们提供任何功能,则将其视为已解决。实现已解析函数的单个目标文件完全添加到二进制文件中。
他们也可能有要求,这些是: -
请注意,库的顺序很重要,因为只有部分库需要添加到二进制文件中。
如果此过程成功,则在Windows上,然后添加了所需的所有功能。
在unix上,您可能需要通过-z,defs
SO : unresolved externals。这允许unix .so通过加载二进制文件来满足它的一些要求,但是可能导致二进制文件不完整。
二进制文件有: -
shared objects
及其功能列表。