ライブラリを作ろう

以前、RCC2017アドカレ17日目を書いたので、その転載です。

 

 

12月の滋賀寒すぎでしょ。こんにちは、しゅもんです。

 

 

さっそくですが、みなさん「ライブラリ」はご存知ですか?

知っていても知っていなくても普段とってもお世話になっているはずです。

 

今日は何かと敬遠されがちなライブラリの話をします。

(先に断っておきますが、この記事におけるライブラリとはLinuxライブラリのことで、Windowsライブラリについては全く言及していないので、.dllの細かい挙動とかは自分で調べてください。そして僕に教えてください。)

 

 

ライブラリってなに?

ライブラリ: Library)は、汎用性の高い複数のプログラムを再利用可能な形でひとまとまりにしたものである。ライブラリと呼ぶ時は、それ単体ではプログラムとして作動させることはできない実行ファイルではない場合がある。ライブラリは他のプログラムに何らかの機能を提供するコードの集まりと言うことができる。ソースコードの場合と、オブジェクトコード、あるいは専用の形式を用いる場合とがある。

Wikipediaにはこう書いてあって、イマイチよく分からないかもしれませんが、今日は一番最後の専用の形式を用いる場合についての話をします。

「普段お世話になっている」と言いましたが、どんなときお世話になっているのでしょう?

ここから少しずつ掘り下げて解説していきたいと思います。

 

 

printf()はどこからきたの?

例えばC言語Hello Worldを書いてみると

こんな感じになりますね(C言語やってない1回生ごめんね)

普段何気なく使っているprintf()ですが、関数を実装せずに使えるのはなぜでしょう?

おそらくC言語の入門書を一冊読んだ人なら、stdio.hの中に書いてあるから』と答える人が一定数いると思いますが、本当にそうでしょうか?

stdio.hの中身を見てみましょう。

これはGCC-6.4.0のstdioヘッダの中身です。目を凝らして読み込んでもらうかブラウザの検索機能を使うと分かりますが、printf()については一切の記述がありません。

いくつかヘッダ中でincludeされているヘッダがあるので、さらにprintf()を探してみます。

#include_next<stdio.h>というのが怪しいですね。

#include_nextというのは見たことない人がほとんどだと思いますが、これはGNU拡張のプリプロセッサ指令です。

インクルードサーチパス内で2つ目に見つかったヘッダをインクルードする少し特殊な指令なのでインクルードパスを調べるためにコンパイル中に実行されるコマンドを-vオプションを使って眺めてみましょう。

$ gcc -v HelloWorld.c
Using built-in specs.
COLLECT_GCC=gcc-6
COLLECT_LTO_WRAPPER=/usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/lto-wrapper
Target: x86_64-apple-darwin17.2.0
Configured with: ../configure --build=x86_64-apple-darwin17.2.0 --prefix=/usr/local/Cellar/gcc@6/6.4.0 --libdir=/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6 --enable-languages=c,c++,objc,obj-c++,fortran --program-suffix=-6 --with-gmp=/usr/local/opt/gmp --with-mpfr=/usr/local/opt/mpfr --with-mpc=/usr/local/opt/libmpc --with-isl=/usr/local/opt/isl --with-system-zlib --enable-stage1-checking --enable-checking=release --enable-lto --with-build-config=bootstrap-debug --disable-werror --with-pkgversion='Homebrew GCC 6.4.0' --with-bugurl=https://github.com/Homebrew/homebrew-core/issues --disable-nls --enable-multilib --with-native-system-header-dir=/usr/include --with-sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk
Thread model: posix
gcc version 6.4.0 (Homebrew GCC 6.4.0) 
COLLECT_GCC_OPTIONS='-v' '-mmacosx-version-min=10.13.2' '-asm_macosx_version_min=10.13' '-mtune=core2'
 /usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/cc1 -quiet -v -D__DYNAMIC__ HelloWorld.c -fPIC -quiet -dumpbase HelloWorld.c -mmacosx-version-min=10.13.2 -mtune=core2 -auxbase HelloWorld -version -o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccytH2Cl.s
GNU C11 (Homebrew GCC 6.4.0) version 6.4.0 (x86_64-apple-darwin17.2.0)
	compiled by GNU C version 6.4.0, GMP version 6.1.2, MPFR version 3.1.6, MPC version 1.0.3, isl version 0.15
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/local/include"
ignoring nonexistent directory "/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/../../../../../../x86_64-apple-darwin17.2.0/include"
ignoring nonexistent directory "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/Library/Frameworks"
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/include
 /usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/include-fixed
 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include
 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks
End of search list.
GNU C11 (Homebrew GCC 6.4.0) version 6.4.0 (x86_64-apple-darwin17.2.0)
	compiled by GNU C version 6.4.0, GMP version 6.1.2, MPFR version 3.1.6, MPC version 1.0.3, isl version 0.15
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 63220a0f81d9fbb8407aeecf817f8077
COLLECT_GCC_OPTIONS='-v' '-mmacosx-version-min=10.13.2'  '-mtune=core2'
 as -arch x86_64 -v -force_cpusubtype_ALL -mmacosx-version-min=10.13 -o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccytH2Cl.s
Apple LLVM version 9.0.0 (clang-900.0.39.2)
Target: x86_64-apple-darwin17.2.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang" -cc1as -triple x86_64-apple-macosx10.13.0 -filetype obj -main-file-name ccytH2Cl.s -target-cpu penryn -fdebug-compilation-dir /Users/shumon -dwarf-debug-producer Apple LLVM version 9.0.0 (clang-900.0.39.2) -dwarf-version=4 -mrelocation-model pic -o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccytH2Cl.s
COMPILER_PATH=/usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/:/usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/:/usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/:/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/:/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/
LIBRARY_PATH=/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/:/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/../../../
COLLECT_GCC_OPTIONS='-v' '-mmacosx-version-min=10.13.2'  '-mtune=core2'
 /usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/collect2 -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/ -dynamic -arch x86_64 -macosx_version_min 10.13.2 -weak_reference_mismatches non-weak -o a.out -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0 -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/../../.. /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o -no_compact_unwind -lSystem -lgcc_ext.10.5 -lgcc -lSystem -v
collect2 version 6.4.0
/usr/bin/ld -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/ -dynamic -arch x86_64 -macosx_version_min 10.13.2 -weak_reference_mismatches non-weak -o a.out -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0 -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/../../.. /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o -no_compact_unwind -lSystem -lgcc_ext.10.5 -lgcc -lSystem -v
@(#)PROGRAM:ld  PROJECT:ld64-305
configured to support archs: armv6 armv7 armv7s arm64 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em (tvOS)
Library search paths:
	/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0
	/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/lib
Framework search paths:
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks/
 /usr/bin/nm -n /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o

このうちのサーチパスの部分だけを抜き出してみると

#include "..." search starts here:
#include <...> search starts here:
 /usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/include
 /usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/include-fixed
 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include
 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks
End of search list.

最初に確認したstdio.hは

/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/include/ssp/stdio.h

のものなので、この次に見つかるstdio.hを探してみると

/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include/stdio.h

がありました。

さっそく中身を読んでいきたいところですが、500行くらいあるので、ここでは省略して書いています。

『__printflikeってなんやねん』というわけでさらに掘り進めてみます。

先ほどと同じくヘッダ中でインクルードされているヘッダから__printflikeを探してみると

/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include/sys/cdefs.h

に書かれていました。

cdefs.hもめちゃくちゃ長いので必要な部分以外は省略します。

また新しいものが出てきましたね。

__attribute__と__format__と__printf__というマクロが登場したのでまだまだ探っていきたいですが、さすがに誰もついてきてなさそうなのでこのへんで解説をはさみます。

__attribute__はCコンパイラGNU拡張で、関数宣言に対して関数属性というものを指定できます。いきなりこんなことを言われても意味が分からないかもしれませんが、関数属性を指定することで、コンパイル時に指定された属性に従って特殊な機能を使うことができます。

__attribute__の話だけでアドカレが書けてしまうので、ここではそういう便利なものがあるとだけ知っていてください(すでにC言語の標準規格からは外れているのでgccでないとコンパイルできないことに注意してください)

次に__format__についてですが、指定したformat属性に反する場合は警告文を出してくれるようになります(ただし-Wallオプションを付ける必要がある。もっと厳密に言うと-Wformatオプションが必要になる)

format属性は3つの引数を取り、

    第一引数にフォーマットの種類
    第二引数にフォーマット文字列の引数の位置
    第三引数にフォーマット文字列でチェックされる引数の最初の位置

を指定します。

もう何言ってるのか意味不明って感じですが、フォーマット文字列というのはprintf()とかscanf()の第一引数に使っている%dとか%fみたいなフォーマット指定子の入っている文字列のことです。

__printf__はprintf用のフォーマットであることを表すただのシンボルです。

つまり、

これはfmtarg番目の引数のフォーマット文字列が、firstvararg番目以降の引数で正しく出力できるかどうかをコンパイル時に確認し、不正な場合は警告を出すことができるマクロです。

さらにprintf()の宣言に戻ってみると、

これは「printf()の1番目の引数のフォーマット文字列が、2番目以降の引数で正しく出力できるかどうかをコンパイル時に確認し、不正な場合は警告を出す」ということになります。

ここまで200行を超える長文を書いておいてアレなんですけど、要するに__printflikeは本質的に重要な意味を持っていません。

ここで最初の「printf()はどこからきたの?」という問いに立ち返ってみると、

「結局printf()の中身って書かれてなくね?」

そう、書かれていないのです。

この中身こそライブラリです

stdio.hの中に書いてあるから』と答えてしまった人は、自分でヘッダファイルを書いてみると、どうして関数の中身を書かないのかが分かると思います。

ただ、もし「宣言だけ書いてある」という意味で答えていたなら正しいです。

 

 

結局中身はどこにあるの?

ライブラリは、よく使う関数などを予めコンパイルしておくことで、ソースコードをコンパクトにし、コンパイル時間を短縮するためのものです(コンパイル済みなのでasciiではなくbainary)

ライブラリを使うにはコンパイル時にリンクする必要があります。

もう一度、コンパイルの流れを見てみましょう。

$ gcc -v HelloWorld.c
Using built-in specs.
COLLECT_GCC=gcc-6
COLLECT_LTO_WRAPPER=/usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/lto-wrapper
Target: x86_64-apple-darwin17.2.0
Configured with: ../configure --build=x86_64-apple-darwin17.2.0 --prefix=/usr/local/Cellar/gcc@6/6.4.0 --libdir=/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6 --enable-languages=c,c++,objc,obj-c++,fortran --program-suffix=-6 --with-gmp=/usr/local/opt/gmp --with-mpfr=/usr/local/opt/mpfr --with-mpc=/usr/local/opt/libmpc --with-isl=/usr/local/opt/isl --with-system-zlib --enable-stage1-checking --enable-checking=release --enable-lto --with-build-config=bootstrap-debug --disable-werror --with-pkgversion='Homebrew GCC 6.4.0' --with-bugurl=https://github.com/Homebrew/homebrew-core/issues --disable-nls --enable-multilib --with-native-system-header-dir=/usr/include --with-sysroot=/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk
Thread model: posix
gcc version 6.4.0 (Homebrew GCC 6.4.0) 
COLLECT_GCC_OPTIONS='-v' '-mmacosx-version-min=10.13.2' '-asm_macosx_version_min=10.13' '-mtune=core2'
 /usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/cc1 -quiet -v -D__DYNAMIC__ HelloWorld.c -fPIC -quiet -dumpbase HelloWorld.c -mmacosx-version-min=10.13.2 -mtune=core2 -auxbase HelloWorld -version -o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccytH2Cl.s
GNU C11 (Homebrew GCC 6.4.0) version 6.4.0 (x86_64-apple-darwin17.2.0)
	compiled by GNU C version 6.4.0, GMP version 6.1.2, MPFR version 3.1.6, MPC version 1.0.3, isl version 0.15
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
ignoring nonexistent directory "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/local/include"
ignoring nonexistent directory "/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/../../../../../../x86_64-apple-darwin17.2.0/include"
ignoring nonexistent directory "/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/Library/Frameworks"
#include "..." search starts here:
#include <...> search starts here:
 /usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/include
 /usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/include-fixed
 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/include
 /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks
End of search list.
GNU C11 (Homebrew GCC 6.4.0) version 6.4.0 (x86_64-apple-darwin17.2.0)
	compiled by GNU C version 6.4.0, GMP version 6.1.2, MPFR version 3.1.6, MPC version 1.0.3, isl version 0.15
GGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072
Compiler executable checksum: 63220a0f81d9fbb8407aeecf817f8077
COLLECT_GCC_OPTIONS='-v' '-mmacosx-version-min=10.13.2'  '-mtune=core2'
 as -arch x86_64 -v -force_cpusubtype_ALL -mmacosx-version-min=10.13 -o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccytH2Cl.s
Apple LLVM version 9.0.0 (clang-900.0.39.2)
Target: x86_64-apple-darwin17.2.0
Thread model: posix
InstalledDir: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin
 "/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang" -cc1as -triple x86_64-apple-macosx10.13.0 -filetype obj -main-file-name ccytH2Cl.s -target-cpu penryn -fdebug-compilation-dir /Users/shumon -dwarf-debug-producer Apple LLVM version 9.0.0 (clang-900.0.39.2) -dwarf-version=4 -mrelocation-model pic -o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccytH2Cl.s
COMPILER_PATH=/usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/:/usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/:/usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/:/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/:/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/
LIBRARY_PATH=/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/:/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/../../../
COLLECT_GCC_OPTIONS='-v' '-mmacosx-version-min=10.13.2'  '-mtune=core2'
 /usr/local/Cellar/gcc@6/6.4.0/libexec/gcc/x86_64-apple-darwin17.2.0/6.4.0/collect2 -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/ -dynamic -arch x86_64 -macosx_version_min 10.13.2 -weak_reference_mismatches non-weak -o a.out -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0 -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/../../.. /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o -no_compact_unwind -lSystem -lgcc_ext.10.5 -lgcc -lSystem -v
collect2 version 6.4.0
/usr/bin/ld -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/ -dynamic -arch x86_64 -macosx_version_min 10.13.2 -weak_reference_mismatches non-weak -o a.out -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0 -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/../../.. /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o -no_compact_unwind -lSystem -lgcc_ext.10.5 -lgcc -lSystem -v
@(#)PROGRAM:ld  PROJECT:ld64-305
configured to support archs: armv6 armv7 armv7s arm64 i386 x86_64 x86_64h armv6m armv7k armv7m armv7em (tvOS)
Library search paths:
	/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0
	/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/usr/lib
Framework search paths:
	/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/System/Library/Frameworks/
 /usr/bin/nm -n /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o

コンパイルの最終段階でファイルをリンクし、実行可能な形式にするldコマンドを呼び出している部分を見てみましょう。

/usr/bin/ld -syslibroot /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.13.sdk/ -dynamic -arch x86_64 -macosx_version_min 10.13.2 -weak_reference_mismatches non-weak -o a.out -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0 -L/usr/local/Cellar/gcc@6/6.4.0/lib/gcc/6/gcc/x86_64-apple-darwin17.2.0/6.4.0/../../.. /var/folders/m9/mpb36w6n2rbd66lxm4fj2x3m0000gn/T//ccrukZNT.o -no_compact_unwind -lSystem -lgcc_ext.10.5 -lgcc -lSystem -v

大量のオプションがあって分かりづらいですが、-lSystemというオプションに注目してください。

-lSystemはライブラリサーチパス内にあるlibSystem.dylibをリンクするという意味です。

このlibSystem.dylibの中に、printf()の中身があります。

これはC言語標準ライブラリと呼ばれるもので、MacだとlibSystem.dylib、LinuxUnixBSDだとlibc.soかlibc.aとかになっているはずです。ちなみにMacもほぼPOSIX準拠なのでlibcは使えます。

(というかlibSystemはlibc、libinfo、libkvm、libm、libpthreadをまとめたもなので当たり前ですね。Mac OS X 10.1以前のバージョンではこれらに加えてlibcursesもlibSystemに含まれていました。)

C言語標準ライブラリにはprintf()以外にもscanf()とか他にもいろんな関数が入っています。

具体的にどんな関数が入っているのか気になる人はGNUのCライブラリの一覧を見てください。

想像以上にたくさんの関数があって驚くと思いますよ。

 

 

ライブラリの種類

ライブラリには実はいくつか種類があり、拡張子との対応は以下のようになっています。

 

それぞれの種類の違いを解説していきます。

まず静的ライブラリについてですが、静的ライブラリの拡張子には*.aが使われます。

これは複数のオブジェクトファイルをまとめただけの単なるアーカイブファイルのため、使い方はオブジェクトファイルとほとんど同じです。(ただしコンパイラに与える引数の順序には注意が必要)

プログラムのコンパイル時にリンクされ、生成された実行形式のバイナリに完全に組み込まれます。

次に共有ライブラリについてですが、これはLinuxMacで少し違いがあります。

 

 

Linuxにおける共有ライブラリ

Linuxの場合は、動的リンクライブラリと動的ロードライブラリに明確な違いはありません。

そのため、拡張子はどちらも*.soです。ただし、現在のELF形式ではなく、古いa.out形式の場合は*.saとなります。(このへんは分からなかったスルーしてください)

どうして同じファイルなのに名前が違うかというと、使い方に違いがあります。

動的リンクライブラリとはプログラムの起動時にリンクされるライブラリで、プログラムの実行時にリンカローダにメモリマップを書き換えてもらうことで使用できます。

複数のプログラムから呼び出される処理を動的リンクすることで、メモリの使用量を削減し、バイナリサイズも抑えることができます。

それに対して動的ロードライブラリとは、プログラムの実行中に呼び出されるライブラリで他のライブラリとは違い、ライブラリがなくてもプログラムを起動することができます。

もしライブラリが存在しない状態でをライブラリを動的ロードしようすると、落ちますが、ライブラリが必要でない処理は問題なく行えます。

ライブラリというより、モジュールに近いイメージですね。

プラグイン」と呼ばれる類のものは動的ロードで実装されていることが多いです。

LinuxUnixBSDの一部とかSolarisではこの2つはファイルフォーマットとしては全く同じものなので、共有ライブラリは自由に動的リンクしたり動的ロードすることができます。

 

 

Macにおける共有ライブラリ

次にMacの場合についてですが、Macで実行形式ファイルはELFではなくMach-Oというファイルフォーマットを使用しています。

これはMacMach-Oを実行形式ファイルに採用しているMachというOSを元に作られたからです。(特に関係ないので、トリビア的に覚えておくと良いかもしれません。)

Macでは動的リンクライブラリと動的ロードライブラリは明確に区別されます。

そもそも区別の仕方が違って、Macでは動的リンクライブラリのことを共有ライブラリ、動的ロードライブラリのことを動的ローダブルモジュールと呼びます。

動的リンクライブラリ(共有ライブラリ)の拡張子は*.dylibでファイルタイプはMH_DYLIBです。*.soは使用できません。Linuxの動的リンクライブラリと同様にプログラムの起動時にリンクされます。

動的ロードライブラリ(動的ローダブルモジュール)の拡張子は*.bundleが推奨されていますが、Linuxとの互換性のため*.soも使用でき、ファイルタイプはMH_BUNDLEです。

まあ正直この辺はちょっとした知識の話なので、そんなに気にしないでください。

 

 

ライブラリを自作してみよう〜静的ライブラリ〜

これからようやく、具体的な話に入っていきます。

gccを使ってライブラリを自作して実際に使ってみましょう。

まずは静的ライブラリから作っていきます。

ここではこんな関数をライブラリ化してみます。

これをコンパイルし、まずはオブジェクトファイルを作成します。

$ gcc -c static.c -o static.o

次にarコマンドを使ってオブジェクトファイルをアーカイブします。

$ ar r libstatic.a static.o

arコマンドの細かい挙動については触れませんが、なかなかおもしろいコマンドなので、一度調べてみると新しい発見があって楽しいですよ。

これで「libstatic.a」という静的ライブラリができました。

「lib」という接頭辞は、後述のリンク時に必要なので、必ずつけてください。

次に、このライブラリを呼び出すプログラムとしてこんなプログラムを書きます。

使いたい関数のプロトタイプ宣言だけを書き、実装は書いていないので、普通にコンパイルしようとすると、

$ gcc hello.c
Undefined symbols for architecture x86_64:
  "_hello", referenced from:
      _main in ccHCMB3p.o
ld: symbol(s) not found for architecture x86_64
collect2: error: ld returned 1 exit status

当然このようにコンパイルエラーとなります。

次にhello()の実装が書かれたlibstatic.aを静的リンクしてコンパイルしてみます。

$ gcc hello.c -L. -lstatic

-Lオプションはライブラリのサーチパスを指定するオプションで、ここではカレントディレクトリを指定しているので、カレントディレクトリにあるライブラリをリンクできるようになります。

次の-lオプションはリンクするライブラリを指定するオプションで、ここではstaticを指定しているので、ライブラリサーチパス内のlibstaticをリンクします。

libから始まるファイルがライブラリとみなされるので、「libと拡張子を除いた名前でリンクするライブラリを指定する」という少し変わった指定方法なので注意してください。

ちなみにmath.hを使うときに-lmというリンクオプションを指定しますが、あれはlibmをリンクするという意味になります。

生成されたバイナリを実行してみます。

$ ./a.out
Hello

このようにきちんとリンクされていることが分かります。

また、直接ライブラリを指定してコンパイルすることもできます。

$ gcc hello.c libstatic.a
$ ./a.out
Hello

 

 

ライブラリを自作してみよう〜動的リンクライブラリ〜

今度は動的リンクライブラリを自作してみます。

先ほどと同じように、ライブラリ化する関数を書きます。

動的リンクライブラリをコンパイルする場合は-sharedオプションと-fPICオプションを使います。

このあたりのオプションの挙動は奥が深いのでこれを読んでください。

$ gcc -shared dlink.c -fPIC -o libdlink.dylib

静的ライブラリと同様に、libを接頭辞にする必要があります。

また、Linuxでは拡張子を*.soにしてください。

ライブラリを呼び出すプログラムは先ほどと同じです。

libdlink.dylibを動的リンクしてコンパイルして実行してみます。

オプションの指定は静的ライブラリのときと同じです。

$ gcc hello.c -L. -ldlink
$ ./a.out
Hello

ここまでは静的ライブラリを使用した場合とほとんど変わりませんが、動的リンクライブラリはプログラムの起動時にリンクされるため、libdlink.dylibを変更すると、a.outの結果も変化します。

dlink.cの出力を「Hello」ではなく、「Hello, World」に変更しました。

再度dlink.cをコンパイルしてみます。

$ gcc -shared dlink.c -fPIC -o libdlink.dylib

これで、libdlink.dylibを変更することができたのでa.outを実行してみます。

$ ./a.out
Hello, World

このように、一切a.outに触れることなく、a.outの挙動を変えることができます。

これ地味にすごくないですか?

僕はこれを知るまで、一度コンパイルしたプログラムを再コンパイルせずに挙動を変えられるなんて考えたこともなかったんですけど、実際に目の前で挙動が変わると、結構感動しました。

ただし、動的リンクによる弊害として、プログラムを起動するには必ずライブラリが必要になります。

ためしに、libdlink.dylibを削除してa.outを実行してみます。

$ rm libdlink.dyilb
$ ./a.out
dyld: Library not loaded: libdlink.dylib
  Referenced from: /Users/shumon/./a.out
  Reason: image not found

このように動的リンクライブラリを使用する場合は、ビルド後のライブラリの扱いに注意してください。特に、作ったプログラムを配布したい場合は、ライブラリもセットで配布する必要があります。

また、静的ライブラリと同様、直接ライブラリを指定してコンパイルすることもできます。

$ gcc hello.c libdlink.dylib
$ ./a.out
Hello, World

 

 

ライブラリを自作してみよう〜動的ロードライブラリ〜

最後に動的ロードライブラリを自作します。

動的ロードライブラリは今までとはかなり様子が違ってきます。

今までは、コンパイルオプションとしてリンクするライブラリを指定していましたが、動的ロードライブラリはリンクを必要としません。

その代わり、ソースコード中にライブラリをロードする処理を書く必要があります。

ライブラリ自体のソースコードを変更する必要はありません。

次に動的ロードライブラリとしてコンパイルしますが、MacLinuxではコンパイル方法が違います。

前述の通りLinuxでは動的リンクライブラリと動的ロードライブラリは同じものなので、コンパイル方法は割愛します。

ここではMacにおける動的ロードライブラリのコンパイルを解説します。

そもそも共有ライブラリはOSによって扱いがかなり違っており、GCC処理系依存オプションを多用するので、(動的リンクライブラリのコンパイルでも)それぞれのOSとアーキテクチャに合わせたオプションを指定する必要があります。

この記事では簡略化のために最低限動作するオプションにとどめていますが、実際に使っていく場合は自分の環境にあったオプションを指定してください。

Macで動的ロードライブラリをコンパイルする場合は、-bundleオプションを指定します。

$ gcc -bundle dload.c -o libdload.bundle

ここでは動的ロードライブラリはソースコード中にロードしたいライブラリのパスを含めるので、実は動的ロードライブラリの場合は必ずしも接頭辞にlibを付ける必要はないんですが、ライブラリだということを明示するためにlibを付けています。

次にライブラリを呼び出すプログラムですが、他のライブラリの呼び出しに比べて少し難易度が上がります。

簡単なエラーハンドリングを書かなければならず、ロードの処理に関数ポインタの知識が必要になります。

関数ポインタについては前提知識として扱うので、知らない人はここを参照してください。

一気にコード量が増えましたね。いくつか動的ロード専用の関数を使っているので、解説していきます。

まずdlfcn.hをインクルードしていますが、これはおまじないで、動的ロードに必要な定義が書かれています。

17行目でlibdload.bundleを動的ロードします。

dlopen()は2つの引数を取り、

    第一引数に動的ロードするライブラリのパスまたはファイル名
    第二引数にバインディングのモード

を指定します。

ライブラリのパス名は絶対パスでも相対パスでもかまいません。

パスなしでファイル名を直接指定した場合は動的リンカのサーチパスの中から検索します。サーチパスについてはここを参照してください。

ここでは相対パスでカレントディレクトリのlibdload.bundleを指定しています。

第二引数にはRTLD_LAZY、またはRTLD_NOWのどちらかを指定します。

RTLD_LAZYを指定すると、シンボルの解決がそのシンボルを参照するコードが実行されるときにのみ行います。

RTLD_NOWを指定すると、ライブラリ中のシンボルを全て解決してからdlopen()は復帰します。

これだけ聞いても意味が分からないと思いますが、要するにライブラリ内の全ての関数の宣言と定義を結びつけるタイミングが、dlopen()を呼び出したときか、ライブラリ内の一度も呼び出されたことのない関数が呼び出される度にか、ということです。

このあたりのリンカの挙動については、Oracleこれ

が詳細にまとめられていて分かりやすいです。Solarisのリンカの話ですが、概ね共通しているので問題ないと思います。

ここではRTLD_LAZYを指定しています。

dlopen()がライブラリの動的ロードに成功した場合、そのライブラリへのハンドルを返し、失敗した場合、NULLを返します。

ハンドルと言われてもよく分からないかもしれませんが、ファイルポインタのようなものだと思ってください(この表現はイメージを掴んでもらうためのものであり、ハンドルの解説としては何もかも違うことに注意してください)

error()はエラーハンドリングのため、説明を後に回します。

20行目でライブラリの中から、hello()の関数ポインタを取り出します。

dlsym()は2つの引数を取り、

    第一引数にdlopen()が返した動的ライブラリのハンドル
    第二引数にシンボル名

を指定します.

第一引数にはそのままhandleを渡しています。

第二引数にシンボル名としてhelloを指定していますが、シンボル名とは、関数名か大域変数名と思ってもらって結構です。(厳密には、リンカが関数、変数を識別するための識別子のこと。)

ここではlibdload.handleの中のhello()を指定していることになります。

dlsym()は指定したシンボルがロードされたメモリのアドレスを返します。シンボルが見つからなかったり、ハンドルが不正だった場合NULLを返します。

ここではlibdload.bundleの中のhello()の関数ポインタが返ってきます。

23行目で、ようやくlibdload.bundleからロードしたhello()を呼び出しています。

25行目でハンドルをクローズしています。他にそのライブラリがオープンされておらず、ロードしている他のライブラリからもそのライブラリ内のシンボルが使われていなければ、そのライブラリをアンロードします。

次に、後回しにしていたエラーハンドリングの解説をします。

dlerror()は、前回dlerror()が呼び出された後に、dlopen()、dlsym()、dlclose()のいずれかで最後に発生したエラーについての説明メッセージを返し、エラーが発生してい場合はNULLを返します。

error()はエラーが発生していた場合は、エラーメッセージを出力し、プログラムを終了。発生していない場合は何もせずに返します。

動的ロードライブラリは関数ポインタを駆使して、ゲームエンジンを自作したりする場合にも使えそうですね。敵の挙動を司る関数を自由に切り替えることで、簡単に完全自作ゲームを1つのフレームワークから作れたりすると、ちょっと面白いかもしれません。

 

 

おわり

ここまでクソ長い記事を書いておいてなんですが、この話一体誰向けの記事なんだ

ここまで全部読んでくれた人は果たして何人いるんだろう......

こんなこと知ってなんになるんだって感じだと思うんですけど、ライブラリを自分で整備できるようになると、C言語で書いた関数を他の言語から呼び出すことができたりします。

例えばサーバサイドをRuby on Railsで書いているけど、特定の処理に時間が掛かってパフォーマンスが出ない......みたいなとき、クリティカルな箇所だけC言語で書き直すことで、100倍高速化!なんてことが簡単にできます。

スクリプト言語の開発効率とコンパイル言語の実行時間を両立することができるって考えれば、ちょっとは有益な情報だったかなって感じるでしょ?

(とは言っても、そのへんを上手に高速かつ書きやすい形で提供してくれているのがフレームワークなので活用する機会は少なそうですが)

あと、ライブラリには関係ないんですが、ちょこちょこ出てきた「GCC専用の拡張機能」は便利な機能もたくさんあって、一度調べてみるとなかなか面白いですよ。

以上、ライブラリの話でした。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

おまけ

実は「スクリプト言語の開発効率とコンパイル言語の実行時間」を両立した

Julia

という言語が存在します。