Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to link dylib in NAppGUI project #165

Open
akacastor opened this issue Dec 6, 2024 · 7 comments
Open

How to link dylib in NAppGUI project #165

akacastor opened this issue Dec 6, 2024 · 7 comments
Assignees
Labels
help wanted Extra attention is needed

Comments

@akacastor
Copy link

I have a .dylib and .h file for a third-party library that I want to link with my NAppGUI project on macOS.

What should I add to CMakeLists.txt in order to link a .dylib with my application?

Follow-up question: Is the process the same for macOS and other platforms (Linux, Windows)?

@frang75
Copy link
Owner

frang75 commented Dec 6, 2024

Hi @akacastor!

You have to add these lines to your CMakeLists.txt:

nap_desktop_app(HelloWorld "osapp" NRC_NONE)
set_target_properties(HelloWorld PROPERTIES FOLDER "demo")
...
target_include_directories(HelloWorld PUBLIC "/Users/fran/Desktop")   # For locate your 'dylib.h'
target_link_libraries(HelloWorld "/Users/fran/Desktop/libdylib.dylib")    # For link with your 'libdylib.dylib'

In this case you can use your shared-lib as static-lib

/* Dynamic library for testing */

#ifdef  __cplusplus
extern "C" {
#endif

#include <stdint.h>

uint32_t dylib_add_int(const uint32_t a, const uint32_t b);

float dylib_add_real(const float a, const float b);

extern const uint32_t kDYLIB_INTEGER_THREE;

extern const float kDYLIB_REAL_PI;

#ifdef  __cplusplus
}
#endif
macos

Cross-platform static linking

For Windows/Linux the process is the same, but you have to take into account this details:

  • In Windows, when the DLL is compiled, the exported functions must be preceded by __declspec(dllexport) and, when they are used in your app, must be preceded by __declspec(dllimport).
  • In Windows, when you compile a DLL two libraries will be generated:
    • .lib: This is the one your application should link in target_link_libraries. It contains the exportable symbols.
    • .dll: Contains the binaries. It is the library that your application will look for at runtime.

Take a look to this header: https://github.com/frang75/nappgui_src/blob/main/src/core/core.def
Contains the definition of the cross-platform macro core_api_t that precedes each exportable function in core.lib

@frang75 frang75 self-assigned this Dec 6, 2024
@frang75 frang75 added the help wanted Extra attention is needed label Dec 6, 2024
@akacastor
Copy link
Author

Thank you for the quick and helpful answer! Using target_include_directories and target_link_libraries I am able to build an application linked to the library when building in macOS. (I haven't had a chance to try building in Win/Linux yet, it should be straightforward with your instructions)

Another macOS question - can I include the .dylib inside the application bundle, so the library doesn't have to be installed separately from the application?

From some reading, I think it is possible, for example: https://stackoverflow.com/questions/33333628/how-can-i-bundle-a-dylib-within-a-mac-app-bundle-in-qt

There are a few issues I don't fully understand:

Where should the .dylib be placed? I guess within Contents/Frameworks/ or alongside the executable in Contents/MacOS/?

How to copy the .dylib into the application bundle as part of the build process?

How to set RPATH correctly for the .dylib if it is inside the application bundle? Is there a CMake directive to specify the correct RPATH during compilation, or would it be done using by calling install_name_tool to change the path after compilation (as done in the above stackoverflow link)?

@frang75
Copy link
Owner

frang75 commented Dec 8, 2024

Try this, It works for the HelloWorld example:

nap_desktop_app(HelloWorld "osapp" NRC_NONE)
set_target_properties(HelloWorld PROPERTIES FOLDER "demo")
target_include_directories(HelloWorld PUBLIC "/Users/fran/Desktop")
target_link_libraries(HelloWorld "/Users/fran/Desktop/libdylib.dylib")

# This will copy your dylib into `/Contents/MacOS` folder inside the bundle.
add_custom_command(TARGET HelloWorld POST_BUILD COMMAND ${CMAKE_COMMAND} -E copy "/Users/fran/Desktop/libdylib.dylib" "$<TARGET_FILE_DIR:HelloWorld>")

# This will write the RPATH of the executable so that it looks for DLLs in the same directory as the executable.
set_property(TARGET HelloWorld PROPERTY SKIP_BUILD_RPATH FALSE)
set_property(TARGET HelloWorld PROPERTY BUILD_RPATH "@executable_path/.")

You can check that the RPATH is written correctly using otool

pwd
/Users/fran/nappgui_svn/trunk/build/Debug/bin/HelloWorld.app/Contents/MacOS
otool -l ./HelloWorld

... 
compatibility version 1.0.0
Load command 24
          cmd LC_RPATH
      cmdsize 32
         path @executable_path/. (offset 12)
...

For Windows it is a bit simpler. Just save a copy of the DLL in the same directory as the executable. Windows, by default, first looks for DLLs in the same directory as the executable.

@akacastor
Copy link
Author

Using the example above, I am able to build my app for macOS with the .dylib included in the bundle. Thank you!

One issue I ran into was that the .dylib was not compiled by me, and therefore not signed by me, and then code signing for the app bundle would fail because of the signature mismatch.

    Signing Identity:     "Sign to Run Locally"
    
    /usr/bin/codesign --force --sign - --entitlements /Users/chrisgerlinsky/proj/picturizatron2/build/build/picturizatron2.build/Debug/picturizatron2.app.xcent --timestamp\=none --generate-entitlement-der /Users/chrisgerlinsky/proj/picturizatron2/build/Debug/bin/picturizatron2.app
/Users/chrisgerlinsky/proj/picturizatron2/build/Debug/bin/picturizatron2.app: code object is not signed at all
In subcomponent: /Users/chrisgerlinsky/proj/picturizatron2/build/Debug/bin/picturizatron2.app/Contents/MacOS/libtoupcam.dylib
Command CodeSign failed with a nonzero exit code

note: Run script build phase 'Generate CMakeFiles/ALL_BUILD' will be run during every build because the option to run the script phase "Based on dependency analysis" is unchecked. (in target 'ALL_BUILD' from project 'picturizatron2')
note: Run script build phase 'CMake PostBuild Rules' will be run during every build because the option to run the script phase "Based on dependency analysis" is unchecked. (in target 'picturizatron2' from project 'picturizatron2')
note: Run script build phase 'Generate CMakeFiles/ZERO_CHECK' will be run during every build because the option to run the script phase "Based on dependency analysis" is unchecked. (in target 'ZERO_CHECK' from project 'picturizatron2')
** BUILD FAILED **


The following build commands failed:
	CodeSign /Users/chrisgerlinsky/proj/picturizatron2/build/Debug/bin/picturizatron2.app (in target 'picturizatron2' from project 'picturizatron2')

This was solved by replacing the signature on the .dylib with my own signature. Conveniently I copied the command line from the above CMake output and specified my .dylib filename.

/usr/bin/codesign --force --sign - --entitlements /Users/chrisgerlinsky/proj/picturizatron2/build/build/picturizatron2.build/Debug/picturizatron2.app.xcent --timestamp\=none --generate-entitlement-der libtoupcam.dylib
libtoupcam.dylib: replacing existing signature

With the .dylib correctly signed, the build process succeeds and the app bundle includes the .dylib and the signatures match.

@frang75
Copy link
Owner

frang75 commented Dec 13, 2024

I'm glad it finally works. I've never had this problem before, as I don't usually use third-party DLLs. But I'll make a note of this idea in case I ever encounter this problem.

@akacastor
Copy link
Author

One very small 'loose end' I have found with including a .dylib inside the .app bundle - the RPATH from target_link_libraries() remains in the .app, alongside the fixed RPATH set by set_property BUILD_RPATH.

The result functions properly, as the .dylib is still found, but in my case the path used for target_link_libraries() is within my project folder and my home directory, so the path /Users/chrisgerlinsky/proj/picturizatron2/src/../lib is left in the binary. I don't consider it a serious problem, but it's maybe a bit of unexpected information leak from the computer used for the build.

The superfluous RPATH from target_link_libraries() can be removed using install_name_tool -delete_rpath, or during the build process the parameter -change could be used instead of -add_rpath to change the first RPATH entry instead of adding another entry. (but I am not familiar enough with the CMake system to make these changes)

At this time, my solution if I need to distribute a binary without the target_link_libraries RPATH is to manually run install_name_tool -delete_rpath target_link_libraries_path app_bundle_path and then re-run codesign to re-sign the executable.

@frang75
Copy link
Owner

frang75 commented Dec 14, 2024

Thanks for sharing. I'll keep this case in mind for future build system revisions on macOS. I'll see if I can integrate a CMake-based solution for this case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

2 participants