Cocoa 封装 C++ 代码的包的动态运行时加载

Dynamic runtime loading of Cocoa Bundles that wrap C++ Code

我想使用类似插件的架构来将模块加载到基于 Cocoa 的应用程序中。所有模块都使用相同的 API,但模块的名称和数量可能会有所不同,并且在构建应用程序时是未知的。

目前,我正在使用静态库,但这需要我在每次添加或删除模块时重新编译应用程序。我希望能够动态地执行此操作 - 即重新启动我的应用程序以更新作为文件添加的模块列表。

我正在考虑两种方法:

  1. 使用动态库(.dylib 文件)并在运行时使用 dlopen() 和 dlsym() 加载它们
  2. 使用捆绑包(.bundle 文件)并在运行时使用 Cocoa 函数加载它们

更复杂的是,模块的代码是 C++(遗留代码),接口如下:

// MyModule_API.h 
class MyModule_API {
public:
    static MyModule* create();
    static void destroy(MyModule* m);

    virtual void processMap(std::map<std::string, float>) = 0;
    virtual std::vector<std::string> getNames() = 0;
}

一个当前的静态模块将这样定义(该模块实现了静态 create/destroy 函数和 API 的其余部分):

#include "MyModule_API.h" 
class MyModule : MyModule_API {
public:
    explicit MyModule(std::string param1, std::string param2) : _param1(param1), _param2(param2) { }
    ~MyModule() { };
    // MyModule_API:
    void processMap(std::map<std::string, float>) override { ... }
    std::vector<std::string> getNames() override { return std::vector<std::string({_param1, _param2}); }
private:
    std::string _param1, _param2;
}

MyModule_API* MyModule_API::create() {
    MyModule* m = new MyModule("foo", "bar");        
    //cast to base/API class before returning
    return (MyModule_API*) m;
}

void MyModule_API::destroy(MyModule_API* m) {
    if (m != nullptr) {
        delete m;
    }
}

由于 C++ 中的名称修改,使用 dylib 方法似乎不可行,因为如果不对符号进行硬编码,应用程序将无法通过名称定位符号。

因此,我正在尝试对模块使用 Objective-C 包装器,然后将它们作为 NSBundles 从 .app.Resources/Plugins 文件夹中导入。

// MyModule_ObjC.h
#import <Foundation/Foundation.h>
#include "MyModule_API.h"
@interface MyModule_ObjC : NSObject {
    MyModule_API* _myModule;
}
- (id) init;
- (void) dealloc;
- (MyModule_API*) getMyModule;
@end


// MyModule_ObjC.mm
#import "MyModule_ObjC"
@implementation MyModule_ObjC
- (id)init {
    self = [super init];
    if (self) {
        _myModule = MyModule_API::create();
    }
    return self;
}
- (void)dealloc {
    MyModule_API::destroy(_myModule);
}
- (MyModule_API*) getMyModule {
    return _myModule;
}
@end

有了这个,我可以成功地构建一个.bundle 文件。 然后我尝试将这个包导入到基于 Cocoa 的测试应用程序中:

#import <Cocoa/Cocoa.h>
#import <mach-o/dyld.h>
#import "MyModule_ObjC.h"
#include "MyModule_API.h"

int main(int argc, const char * argv[]) {    
    NSBundle *appBundle = [NSBundle mainBundle];
    NSArray *bundlePaths = [appBundle pathsForResourcesOfType:@"bundle" inDirectory:@"PlugIns"];
    for (id bundlePath in bundlePaths) {
        NSBundle* bundle;
        bundle = [NSBundle bundleWithPath:bundlePath];
        [bundle load];

        if (bundle) {
            MyModule_ObjC* moduleAPIClass = [bundle principalClass];
            if (moduleAPIClass) {
                id moduleInstance;
                moduleInstance = [[MyModule_ObjC alloc] init];
                if (moduleInstance) {
                    MyModule_API* module = [moduleInstance getMyModule];
                }
            }
        }
    }
    return 0;
}

但是,链接器无法找到“_OBJC_CLASS_$_MyModule_ObjC”...这是有道理的,因为测试应用程序项目不包含 MyModule_ObjC.mm,仅包含 .h .如果添加 .mm,它将找不到 create/destroy 的静态实现,因为该模块不再是静态链接的。但是,我希望这些 create/destroy 实现在 plugin/bundle.

原则上,我的方法合理吗?
如果没有,您会推荐什么方法来让这个插件架构工作?

在此先感谢您,很抱歉 post。

好吧,你快到了 ;-)。问题在于您对待插件界面的方式。目前编译器无法找到实际的符号,因为它们在链接时是未知的。解决方法很简单:

使用协议代替您的 class 界面。

@protocol MyModule_ObjC <NSObject>

- (MyModule_API*) getMyModule;

@end

在你的插件中你需要实现这个接口和相应的主体class。

// PluginA.h

#import <Foundation/Foundation.h>
#include "MyModule_ObjC.h"

@interface PluginA : NSObject<MyModule_ObjC>

@end


// PluginA.mm

#import "PluginA.h"

@implementation PluginA {

    @private

    MyModule_API* _myModule;
}

- (id)init {

    self = [super init];

    if (self) {

        _myModule = MyModule_API::create();
    }

    return self;
}

- (void)dealloc {

    MyModule_API::destroy(_myModule);
}

- (MyModule_API*) getMyModule {

    return _myModule;
}

@end

在您的应用程序中,您需要根据协议加载插件:

#import <Cocoa/Cocoa.h>
#import <mach-o/dyld.h>
#import "MyModule_ObjC.h"
#include "MyModule_API.h"

int main(int argc, const char * argv[]) {    
    NSBundle *appBundle = [NSBundle mainBundle];
    NSArray *bundlePaths = [appBundle pathsForResourcesOfType:@"bundle" inDirectory:@"PlugIns"];
    for (id bundlePath in bundlePaths) {
        NSBundle* bundle;
        bundle = [NSBundle bundleWithPath:bundlePath];
        [bundle load];

        if (bundle) {
            Class moduleAPIClass = [bundle principalClass];
            if (moduleAPIClass && [moduleAPIClass conformsToProtocol:@protocol(MyModule_ObjC)]) {
                id<MyModule_ObjC> moduleInstance;
                moduleInstance = [[moduleAPIClass alloc] init];
            }
        }
    }
    return 0;
}