插件如何在运行时知道查询?
在不同的编译单元(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( &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)以查看它们是否提供了完成完整流程所需的功能。如果他们确实提供了任何功能,那么这将被视为已解决。实现解析功能的单个目标文件被完全添加到二进制文件中。
他们也可能有要求,这些是:-
- 已由已加载的二进制文件解决
- 添加到未解析的值。
请注意,库的顺序很重要,因为只有所需的部分库会添加到二进制文件中。
如果此过程成功,则在 windows 上,所有需要的功能都已添加。
在 unix 上,您可能需要传递 -z,defs SO : unresolved externals。这允许 unix .so 的某些要求由加载的二进制文件来满足,但可能会导致二进制文件不完整。
总结
二进制文件有:-
- 链接命令行中的所有目标文件。
- 满足未解析的外部要求的静态库中的任何目标文件
-
shared objects 的列表及其交付工作程序所需的功能。
- 使用接口和基类,允许在原始设计完成后添加新类。