我正在编写一个程序(macOS、clang++ 编译器,目前仅 AppleSilicon),稍后我可以通过提供使用主程序的公共接口的自定义插件(动态库,在运行时加载)来扩展该程序。
test.hpp - 公共接口:
#if defined(MAGIC_PLUGIN)
# define MAGIC_EXPORT /* nothing */
#else
# define MAGIC_EXPORT __attribute__((visibility("default")))
#endif
MAGIC_EXPORT
void testCall();
test.cpp - 主程序:
#include <stdio.h>
#include <dlfcn.h>
#include "test.hpp"
// Declare a function to call from a loaded plugin
typedef void (* plugin_call_func)(void);
int main(int argc, char** argv) {
// Load library
const char* libraryName = "plugin.dylib";
void* library = dlopen(libraryName, RTLD_NOW);
if (library == nullptr) {
printf("Cannot open library\n");
return 1;
}
// Get function from loaded library
plugin_call_func pluginCall = reinterpret_cast<plugin_call_func>(
dlsym(library, "pluginCall"));
if (pluginCall == nullptr) {
printf("Cannot find the pluginCall function\n");
return 2;
}
// Execute loaded function
pluginCall();
// Forget function and close library
pluginCall = nullptr;
auto libraryCloseResult = dlclose(library);
if (libraryCloseResult != 0) {
printf("Cannot close library\n");
return 3;
}
return 0;
}
// Public function, should be called from a plugin
void testCall() {
printf("Test call\n");
}
plugin.cpp - 插件来源:
#define MAGIC_PLUGIN
#include <stdio.h>
#include "test.hpp"
__attribute__((visibility("default")))
extern "C" void pluginCall(void) {
printf("Plugin call\n");
testCall();
}
首先,我编译主应用程序:
clang++ -std=c++20 -fvisibility=hidden -target arm64-apple-macos12 test.cpp -o test
nm --defined-only test
显示这些符号:
0000000100003ee4 T __Z8testCallv
0000000100000000 T __mh_execute_header
0000000100003dcc t _main
Mangled __Z8testCallv 是我所需要的。到目前为止一切看起来都很好。但后来我尝试将插件编译为动态库...
clang++ -std=c++20 -fvisibility=hidden -dynamiclib -g -current_version 0.1 -target arm64-apple-macos12 plugin.cpp -o plugin.dylib
并收到此错误:
Undefined symbols for architecture arm64:
"testCall()", referenced from:
_pluginCall in plugin-38422c.o
ld: symbol(s) not found for architecture arm64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
嗯,这有点公平,我可以理解这一点,因为动态库不知道
testCall
是在某个地方实现的。所以我想说它不用担心testCall
的存在。
我尝试研究如何做到这一点,查找手册页,阅读大量堆栈溢出答案,我发现有效的方法是将这些标志添加到链接器:
-Wl,-undefined,dynamic_lookup
它可以工作,库可以编译并且应用程序可以按预期工作。但我真的不想使用
dynamic_lookup
,因为它会将库中每个未定义的符号标记为已解决,这可能会导致一些不良后果。我只想告诉链接器主程序公共符号的存在。
我错过了什么?还有比
dynamic_lookup
更好的解决方案吗?
您最好的选择是手动完成库加载器完成的工作。即:填充函数指针。毕竟,插件->主绑定已经手动完成,因此以相反的方式做同样的事情是有意义的。
您可以通过精心设计插件和主应用程序共享的标头来使此过程本质上透明。唯一棘手的部分是处理由多个源文件组成的插件的 ODR。
由于这是一个 C++ 问题,因此使用 RAII 包装器可能会如下所示。 ODR 难题是通过
PLUGIN_MAIN
宏来处理的,该宏只能在插件的源代码之一中定义。
test_plugin.hpp
using pluginCall_fn = void(*)();
using testCall_fn = void(*)();
#if !defined(MAIN_APPLICATION)
#if defined(PLUGIN_MAIN)
#define EXPORTED_FROM_MAIN __attribute__((visibility("default")))
#else
#define EXPORTED_FROM_MAIN __attribute__((visibility("default"))) extern
#endif
extern "C" {
// Declare symbols provided by the plugin
__attribute__((visibility("default"))) void pluginCall();
// etc...
// Declare/define pointers that will be populated by the main application
EXPORTED_FROM_MAIN testCall_fn testCall;
// etc...
}
#undef EXPORTED_FROM_MAIN
#else // In the main app.
#include <stdexcept>
// Declare "symbols" provided by the main application
void testCall();
// Utility class to load/unload a dynamic library.
// Probably belongs in its own header...
struct loaded_library final {
loaded_library(const char* libName)
: handle_(dlopen(libName, RTLD_NOW)) {
if(!handle_) {
throw std::runtime_error("failed to load plugin");
}
}
loaded_library(const loaded_library&) = delete;
loaded_library& operator=(const loaded_library&) = delete;
loaded_library(loaded_library&& rhs) : handle_(rhs.handle_) {
rhs.handle_ = nullptr;
}
loaded_library& operator=(loaded_library&& rhs) {
handle_ = rhs.handle_;
rhs.handle_ = nullptr;
return *this;
}
~loaded_library() {
if(handle_) {
dlclose(handle_);
}
}
template<typename T>
T get_symbol(const char* symbol) {
T result = reinterpret_cast<T>(dlsym(handle_, symbol));
if(!result) {
throw std::runtime_error("missing symbol");
}
return result;
}
private:
void* handle_;
};
// Plugin interface.
struct loaded_plugin final {
loaded_plugin(const char* libName)
: lib_(libName) {
// Load functions from plugin
pluginCall = lib_.get_symbol<pluginCall_fn>("pluginCall");
// ...
// Assign callbacks to plugin
*lib_.get_symbol<testCall_fn*>("testCall") = &testCall;
// ...
// Call the plugin's init function here if applicable.
}
pluginCall_fn pluginCall;
private:
loaded_library lib_;
};
#endif
插件.cpp
#define PLUGIN_MAIN
#include "test_plugin.hpp"
#include <stdio.h>
void pluginCall() {
printf("Plugin call\n");
testCall();
}
测试.cpp
#define MAIN_APPLICATION
#include "test_plugin.hpp"
int main(int argc, char** argv) {
const char* libraryName = "plugin.dylib";
loaded_plugin plugin(libraryName);
plugin.pluginCall();
}
// Public function, should be called from a plugin
void testCall() {
printf("Test call\n");
}
您可能会发现这段代码有点脆弱,因为
test_plugin.hpp
的几个不同部分需要保持同步。
这可以通过使用 X-Macros 来解决,但代价是混淆 IDE 并损害代码的可读性。在相关 API 变得大得难以控制之前,我不会走这条路。
谢谢大家的回复! @Frank 的回答确实非常有效。
我发布这个问题已经有一段时间了,我的项目中的很多事情都发生了变化。所有原本可以从插件访问的公共接口现在都存储在可以链接到其他插件的单独框架(编译为动态库)中。因此,这样我就避免了手动导出每个公共符号,这将花费很长时间并且不断减慢我更新它们的速度。
我还没有感觉到记忆或性能受到影响,这种方法对我来说很有效目前。