How to direct `DllImport` to look for `.dylib` placed in the same folder as my custom component (.gha)?

Hello, I have created simplest c++ dylib. that exposes “PostMessage” method,

// PostMessage.h
define ghAPI extern "C"

void _postMessage(const char * s);

ghAPI void PostMessage(const char* s) {
    _postMessage( s );
}

#pragma GCC visibility pop

Then I placed the libPostMessage.dylib under bin/Debug folder,
so Grasshopper plugin can call the method with following:

// MyCustomComponent.cs
...

DllImport("libPostMessage.dylib") PostMessage(string s);

It works fine and looks for the .dylib when I am simply debugging the app, but when I build, and placed the .gha and .dylib in the same directory as ~/Library/Application\ Support/McNeel/Rhinoceros/MacPlugIns/Grasshopper/Libraries/MyCustomComponent/MyCustomComponent.gha, it failed to look up the .dylib.

For now, when I place the .dylib under /usr/local/lib/, the GH component looks it up just fine. but it is better if the .dylib lives in the same directory as .gha.

How should I configure Visual Studio / Xamarin to include the .dylib in the same dir as .gha?

Here’s the simplistic project outlining the setup: → https://github.com/mnmly/Grasshopper_dylib_testbed/

Thanks.

I think you should be able to Pinvoke dlopen to load your shared library before the first usage of your function.

Hi, thank for the quick reply,

I tried it by adding libdl.dylib to access dlopen as following, but didn’t work…


        [DllImport("libdl.dylib")] static extern IntPtr dlopen(String path, int mode);
        [DllImport("libSimple.dylib")] static extern int SimpleFn(int a);
 /// </summary>

        public SimpleDynamicLibTestComponent()
          : base("SimpleDynamicLibTest", "ASpi",
            "Construct an Archimedean, or arithmetic, spiral given its radii and number of turns.",
            "Curve", "Primitive")
        {
            const int RTLD_NOW = 2;
            var ptr = dlopen("/Users/${USERNAME}/Library/Application Support/McNeel/Rhinoceros/MacPlugIns/Grasshopper/Libraries/SimpleDynamicLibTest/libSimple.dylib", RTLD_NOW);

        }

        /// to store data in output parameters.</param>
        protected override void SolveInstance(IGH_DataAccess DA)
        {
            AddRuntimeMessage(GH_RuntimeMessageLevel.Remark, "Result from Simple Fn" + SimpleFn(1).ToString());
        }
...

Even though the `ptr` is returning non-zero value, it still raised `1. Solution exception:libSimple.dylib`.

Found this article: → https://jacksondunstan.com/articles/3945

And realized that I need to get the handler to the .dylib then, get the pointer to the lib, then retrieve the function by its name.


        // [DllImport("libSimple.dylib")] static extern int SimpleFn(int a);

        IntPtr libraryHandle;

        [DllImport("__Internal")]
        public static extern IntPtr dlopen(string path, int flag);

        [DllImport("__Internal")]
        public static extern IntPtr dlsym(IntPtr handle, string symbolName); 

        [DllImport("__Internal")]
        public static extern int dlclose(IntPtr handle);
     
        public static IntPtr OpenLibrary(string path)
        {
            IntPtr handle = dlopen(path, 0);
            if (handle == IntPtr.Zero)
            {
                throw new Exception("Couldn't open native library: " + path);
            }
            return handle;
        }
     
        public static void CloseLibrary(IntPtr libraryHandle)
        {
            dlclose(libraryHandle);
        }
     
        public static T GetDelegate<T>(IntPtr libraryHandle, string functionName) where T : class
        {
            IntPtr symbol = dlsym(libraryHandle, functionName);
            if (symbol == IntPtr.Zero)
            {
                throw new Exception("Couldn't get function: " + functionName);
            }
            return Marshal.GetDelegateForFunctionPointer(symbol, typeof(T)) as T;
        }


        /// <summary>
        /// Each implementation of GH_Component must provide a public 
        /// constructor without any arguments.
        /// Category represents the Tab in which the component will appear, 
        /// Subcategory the panel. If you use non-existing tab or panel names, 
        /// new tabs/panels will automatically be created.
        /// </summary>
        public SimpleDynamicLibTestComponent()
          : base("SimpleDynamicLibTest", "ASpi",
            "Construct an Archimedean, or arithmetic, spiral given its radii and number of turns.",
            "Curve", "Primitive")
        {
            libraryHandle = OpenLibrary("/Users/mnmly/Library/Application Support/McNeel/Rhinoceros/MacPlugIns/Grasshopper/Libraries/SimpleDynamicLibTest/libSimple.dylib");
        }

        public delegate int SimpleFn(int a);

        /// <summary>
        /// Registers all the input parameters for this component.
        /// </summary>
        protected override void RegisterInputParams(GH_Component.GH_InputParamManager pManager)
        {
            // Use the pManager object to register your input parameters.
            // You can often supply default values when creating parameters.
            // All parameters must have the correct access type. If you want 
            // to import lists or trees of values, modify the ParamAccess flag.
            pManager.AddTextParameter("Test", "T", "Test", GH_ParamAccess.item);
            pManager[0].Optional = true;
        }

        /// <summary>
        /// Registers all the output parameters for this component.
        /// </summary>
        protected override void RegisterOutputParams(GH_Component.GH_OutputParamManager pManager)
        {
        }

        /// <summary>
        /// This is the method that actually does the work.
        /// </summary>
        /// <param name="DA">The DA object can be used to retrieve data from input parameters and 
        /// to store data in output parameters.</param>
        protected override void SolveInstance(IGH_DataAccess DA)
        {
            SimpleFn fn = GetDelegate<SimpleFn>(libraryHandle, "SimpleFn");
            AddRuntimeMessage(GH_RuntimeMessageLevel.Remark, "Result from Simple Fn " + fn(10).ToString());
        }

        public override void RemovedFromDocument(GH_Document document)
        {
            CloseLibrary(libraryHandle);
            base.RemovedFromDocument(document);
        }

But then, the next question will be on how I should be getting the .gha's location dynamically.

From your own plug-in you can use the Assembly.GetExecutingAssembly().Location. You have access to your assembly at least through your implementation of GH_AssemblyInfo, which has a property called Assembly.

2 Likes

Awesome, it worked perfectly.

        public SimpleDynamicLibTestComponent()
          : base("SimpleDynamicLibTest", "ASpi",
            "Construct an Archimedean, or arithmetic, spiral given its radii and number of turns.",
            "Curve", "Primitive")
        {

            GH_AssemblyInfo info = Instances.ComponentServer.FindAssembly(new Guid("3a4acc40-c327-4de9-ac25-51b4014c9fd6")); // _Info.cs's GUID
            var path = Path.GetDirectoryName(info.Location);
            libraryHandle = OpenLibrary(path + "/libSimple.dylib");
        }

The repo is updated accordingly for reference.

Thanks a lot!

Just an update, I also found out that I could keep using the DllImport("libName") attributes, by adding Directory.SetCurrentDirectory(ComponentDirectoryPath) as well.

        [DllImport("libSimple.dylib")] static extern int SimpleFn(int a);
 /// </summary>

        public SimpleDynamicLibTestComponent()
          : base("SimpleDynamicLibTest", "ASpi",
            "Construct an Archimedean, or arithmetic, spiral given its radii and number of turns.",
            "Curve", "Primitive")
        {
     GH_AssemblyInfo info = Instances.ComponentServer.FindAssembly(new Guid("3a4acc40-c327-4de9-ac25-51b4014c9fd6")); // _Info.cs's GUID
            var path = Path.GetDirectoryName(info.Location);

            // Setting the path to the current component directory, therefore `.dylib`s put on the same folder will be discovered.
            Directory.SetCurrentDirectory(path);
        }

        /// to store data in output parameters.</param>
        protected override void SolveInstance(IGH_DataAccess DA)
        {
            AddRuntimeMessage(GH_RuntimeMessageLevel.Remark, "Result from Simple Fn " + SimpleFn(10).ToString());
        }

Great,That’s what I want.