me icon indicating copy to clipboard operation
me copied to clipboard

学习 C++ (Part 8: dylib 的编译和运行依赖)

Open nonocast opened this issue 2 years ago • 0 comments

意图

假设我们程序分为app和libcalc.dylib两个部分

app/main.c

int main() {
  int x = add(1, 2);
  printf("1+2=%d\n", x);
  return 0;
}

libcalc.dylib/calc.c

int add(int a, int b) {
  return a + b;
}

实践

  1. 首先新建一个项目目录(hi),然后在建立app, libcalc和include三个目录,同时新建项目级CMake

hi/CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(hi)
add_subdirectory(app)
add_subdirectory(libcalc)
  1. 搞定libcalc
  • 因为calc.h需要共用,所以把它放到hi/include下

hi/include/calc.h

#ifndef CALC_H
#define CALC_H

int add(int a, int b);

#endif

hi/libcalc/calc.c

#include "../include/calc.h"

int add(int a, int b) {
  return a + b;
}

hi/libcalc/CMakeLists.txt

add_library(fooc SHARED calc.c)
  1. 接着搞 app

hi/app/main.c

#include "../include/calc.h"
#include <stdio.h>

int main() {
  int x = add(1, 2);
  printf("1+2=%d\n", x);
  return 0;
}

hi/app/CMakeLists.txt

link_directories($PROJECT_BINARY_DIR}/libcalc)
add_executable(app main.c)
target_link_libraries(app libcalc.dylib)
  1. 编译和运行
➜  hi mkdir build && cd $_
➜  build cmake ..
-- The C compiler identification is AppleClang 13.1.6.13160021
-- The CXX compiler identification is AppleClang 13.1.6.13160021
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /Users/nonocast/Develop/learning/cpp/hi/build
➜  build make
[ 25%] Building C object libcalc/CMakeFiles/calc.dir/calc.c.o
[ 50%] Linking C shared library libcalc.dylib
[ 50%] Built target calc
[ 75%] Building C object app/CMakeFiles/app.dir/main.c.o
[100%] Linking C executable app
[100%] Built target app
➜  build ./app/app
1+2=3

发布

我们在桌面(whatever)新建一个目录xxx模拟发布给其他用户,

➜  build mkdir ~/Desktop/xxx
➜  build cp app/app ~/Desktop/xxx
➜  build ~/Desktop/xxx/app 
1+2=3
➜  build rm -rf libcalc
➜  build ~/Desktop/xxx/app
dyld[62759]: Library not loaded: @rpath/libcalc.dylib
  Referenced from: /Users/nonocast/Desktop/xxx/app
  Reason: tried: '/Users/nonocast/Develop/learning/cpp/hi/build/libcalc/libcalc.dylib' (no such file), '/Users/nonocast/Develop/learning/cpp/hi/build/libcalc/libcalc.dylib' (no such file), '/usr/local/lib/libcalc.dylib' (no such file), '/usr/lib/libcalc.dylib' (no such file)
[1]    62759 abort      ~/Desktop/xxx/app 

你会发现当我仅复制app过去以后是可以运行的,但是删除build下的libcalc后app无法运行,如果此时把libcalc.dylib复制到app同目录下依然无法运行。

这里就引出运行时系统是如何查找dylib的过程,

  • 首先去找编译时link的lib路径,即当时hi/build/libcalc/libcalc.dylib (从报错信息可以发现)
  • 然后根据环境变量DYLD_LIBRARY_PATH进行搜索
  • 最后会去看系统默认的/usr/lib和/usr/local/lib两个目录

所以我们可以借助DYLD_LIBRARY_PATH, DYLD_LIBRARY_PATH=. ./app

➜  xxx ls
app           libcalc.dylib
➜  xxx 
➜  xxx ./app
dyld[63018]: Library not loaded: @rpath/libcalc.dylib
  Referenced from: /Users/nonocast/Desktop/xxx/app
  Reason: tried: '/Users/nonocast/Develop/learning/cpp/hi/build/libcalc/libcalc.dylib' (no such file), '/Users/nonocast/Develop/learning/cpp/hi/build/libcalc/libcalc.dylib' (no such file), '/usr/local/lib/libcalc.dylib' (no such file), '/usr/lib/libcalc.dylib' (no such file)
[1]    63018 abort      ./app
➜  xxx 
➜  xxx DYLD_LIBRARY_PATH=. ./app
1+2=3

但是每次输入DYLD也不是一回事,通过otool -L可以查看app的动态依赖,同时用install_otool_anme去改掉dylib的依赖路径,

➜  xxx otool -L app
app:
        @rpath/libcalc.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.100.3)
➜  xxx 
➜  xxx install_name_tool -change @rpath/libcalc.dylib ./libcalc.dylib app (./libcalc或者libcalc都ok)
➜  xxx 
➜  xxx otool -L app
app:
        ./libcalc.dylib (compatibility version 0.0.0, current version 0.0.0)
        /usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1311.100.3)
➜  xxx 
➜  xxx ./app
1+2=3

搞定。

我们review一下刚才的过程:

  • 首先macOS的本意是dylib是用来共享的,那么就应该在系统层面存放于/usr/lib或者/usr/local/lib
  • 按这个规则,当我们编译app后,按道理说dylib需要在/usr/lib和/usr/local/lib才可以运行,macos为了方便开发,将dylib路径直接写到app中,这样可以在不复制的情况下直接运行app
  • 当发布给其他用户时,也同样是建议将dylib放到/usr/lib或/usr/local/lib,当你不需要和别人共享时就需要通过install_name_tool去修改这个引用地址

延伸到App Bundle

我们将刚才的xxx转为app bundle, 即mv xxx xxx.app,然后参考app bundle的规范布局:

➜  xxx.app tree .
.
└── Contents
    ├── Info.plist
    ├── MacOS
    │   ├── app
    │   └── libcalc.dylib
    └── Resources
        └── AppIcon.icns

3 directories, 4 files

手写一个Info.plist,随便抄一个就完了

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    <key>CFBundleExecutable</key>
    <string>app</string>
    <key>CFBundleIconFile</key>
    <string>AppIcon.icns</string>
    <key>CFBundleIdentifier</key>
    <string>cn.nonocast.app</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>CFBundleSignature</key>
    <string>NONO</string>
    <key>CFBundleVersion</key>
    <string>0.1.2</string>
</dict>
</plist>
  • 其中AppInco.icns从其他app cp一个过来就行

这时候你在terminal里面是可以./app运行的,但通过双击xxx.app就会报错,说找不到libcalc.dylib,很简单,运行路径不是app所在路径,所以这是需要借助@executable_path这个变量,

install_name_tool -change ./libcalc.dylib @executable_path/libcalc.dylib app

一般会将dylib都放在Contents/Framework下面,那就把路径改为@executable_path/Framework/libcalc.dylib就ok了。

Most applications in OS X do not need to use an installer. To make installation and removal easy, OS X provides bundles.

A bundle is basically a directory that contains an application. Unlike normal folders, however, it appears to the user like an ordinary file. The user can double-click a bundle, and the application will launch. Since the bundle is really a directory, it can contain all the support files that are needed for the application.

The reason to use bundles is somewhat obvious if you have installed many applications in OS X: applications in the form of a bundle can be installed simply by dragging the application to the destination folder in the Finder.

There are, however, some situations where bundles are problematic. An application that installs kernel extensions, startup items, system-wide preference panes, or any other system-wide resources cannot be installed by dragging the application, since those resources need to be in a separate place on the drive.

If your application requires installing a startup item, the only practical way to install your application is the use of an installer. OS X makes this easy using PackageMaker. Other commercial solutions are also available from various third parties such as Stuffit InstallerMaker from Aladdin Systems and Installer VISE from MindVision.

In most cases, however, it is preferable to install system-wide components the first time your application is launched. You can do this using Authorization Services, as described in the book Authorization Services Programming Guide, available from the Apple Technical Publications website.

苹果推荐分发程序的时候直接bundle就好了,dmg的目的就是为了压缩,参见 Distributing Your Application, 最后就是你需对的app进行公证,否则是非法开发者,参考:

最后附上代码: hi.zip

参考阅读

nonocast avatar Apr 27 '22 21:04 nonocast