objective-cmacoscocoansbundlesimbl

Cocoa get current running path of SIMBL Plugin


I am working on a SIMBL Plugin that is loaded by a 3rd-party host application on macOS. It is almost entirely written in C++ and has only minimal objective-c components. (The UI is largely provided by API calls into the host app.) One of the requirements is that the plugin bundle can be loaded multiple times from different sub-directories. It is a Lua interpreter, and the goal is for each instance to host a different configuration of lua scripts that appear in separate menus on the host application. Third parties could bundle this plugin with a custom configuration for their script(s) and they would appear as separate items in the app's plugin menu.

This issue I have is this: I need to find out what directory my plugin is executing in. I could create a special class called MY_BUNDLE_ID_CLASS and use:

[NSBundle bundleForClass:[MY_BUNDLE_ID_CLASS class]];

Once I have the correct NSBundle, getting the file path is trivial.

The problem is that if multiple instances of my bundle are loaded (from different folders), Cocoa complains that the class MY_BUNDLE_ID_CLASS is defined in multiple locations and won't guarantee me which one was used. For other similar classes this would be fine for my plugin, because my unique class names are macros that equate to a mangled name that includes the version number, but in this case it isn't okay. It would potentially be multiple instances of the same version. Is there any other way to find out the folder my plugin code is executing from? It seems like a simple request, but I am coming up empty. I welcome suggestions.


Solution

  • Given an address in an executable, the dladdr function can be used to query the dynamic linker about the dynamically-linked image containing that address; i.e., given a reference to a symbol in your plugin, dladdr can give you the dynamic linking information about your plugin.

    The runtime lookup can look as follows:

    // Sample: BundleClass.m, the principal class for the plugin
    
    #import "BundleClass.h"
    #import <dlfcn.h>
    
    // We'll be using a reference to this variable compiled into the plugin,
    // but we can just as easily use a function pointer or similar -- anything
    // that will be statically compiled into the plugin.
    int someVariable = 0;
    
    @implementation BundleClass
    
    + (void)load {
        Dl_info info;
        if (dladdr(&someVariable, &info) != 0) {
            NSLog(@"Plugin loaded from %s", info.dli_fname);
        } else {
            // Handle lookup failure.
        }
    }
    
    @end
    

    Instead of &someSymbol, you can also use a reference to a function (e.g. &someFunctionDefinedInThePlugin), but you should be careful not to pass in a pointer that could be dynamically allocated — since that will likely either fail, or point you to the memory space of the host process.

    On my machine, with a trivial macOS host app setup, the following loading code:

    - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
        NSURL *bundleURL = [[NSBundle mainBundle] URLForResource:@"DynamicBundle" withExtension:@"bundle"];
        if (!bundleURL) {
            NSLog(@"Failed to find bundle!");
            return;
        }
    
        NSLog(@"Bundle class before loading: %@", NSClassFromString(@"BundleClass"));
    
        NSBundle *bundle = [NSBundle bundleWithURL:bundleURL];
        NSError *error = nil;
        if (![bundle loadAndReturnError:&error]) {
            NSLog(@"Failed to load bundle: %@", error);
            return;
        }
    
        NSLog(@"Bundle class after loading: %@", NSClassFromString(@"BundleClass"));
    }
    

    successfully produces

    Bundle class before loading: (null)
    Loaded plugin from /Volumes/ExtSSD/Developer/Xcode/DerivedData/HostApp/Build/Products/Debug/HostApp.app/Contents/Resources/DynamicBundle.bundle/Contents/MacOS/DynamicBundle
    Bundle class after loading: BundleClass
    

    which is indeed the path to the plugin on disk.