Rust의 주요 크레이트 타입들을 예제로 살펴보며 각각을 빌드하고 연결하고 실행하는 방법을 설명합니다.
⊕ 이 글은 Rust 크레이트 연결하기에 관한 계획 중인 연재의 첫 번째 글이다. 내 바람은 이 글들에 대해 비공식적인 피드백을 받고, 그런 다음 이를 Rust 레퍼런스나 어쩌면 Rustonomicon(또는 독립된 책)에 적합한 더 구조화된 내용으로 바꾸는 것이다. Rust 컴파일러를 작업하다 보면, 내가 때때로 마주치는 주제 중 하나는 “내 도구의 이런 특정 기능들을 사용할 때 원래는 어떤 일이 일어나야 하는가?”이다. 좀 더 구체적으로 말하면, Rust에는 컴파일러가 생성하는 오브젝트 코드를 세밀하게 제어할 수 있게 해 주는 여러 비유적인 조절 장치가 있고, 그중 몇몇은 그 코드를 다른 오브젝트 코드에 연결하는 과정과 관련되어 있다.
Rust 레퍼런스의 Linkage 장을 보면 크레이트에는 일곱 가지 종류가 있음을 알 수 있다: bin, lib, dylib, staticlib, cdylib, rlib, 그리고 proc-macro.
⊕ 원래는 일곱 가지를 모두 다루려 했지만, 시간이 늦어지고 글이 길어져서 proc-macro는 다른 날 다루기로 했다.
이 글에서는 위에 나열한 크레이트 타입 중 처음 여섯 가지를 차례로 살펴보면서 다음을 보여줄 것이다: 그런 크레이트의 예제를 어떻게 빌드하는지, 그것에 어떻게 연결하는지, 그리고 그 연결된 크레이트와 함께 어떻게 실행하는지.
이후 글들에서는 연결 단계에 영향을 줄 수 있는 여러 attribute와 명령줄 플래그를 탐구할 것이다. 하지만 지금은 나중 논의를 위한 기초를 세우고 싶다.
⊕ 어떤 현상은 다룰 가치가 있는 코너 케이스를 관찰하기 위해 여러 크레이트 타입을 섞어야 한다. 그것도 미래의 글로 남겨 두겠다.
이 초기 예제들은 가능한 한 단순하다. 우리는 실제로 각 경우가 실행되는 모습을 보여주고 싶다. 대부분의 크레이트 타입은 실행 파일이 아니므로, 거의 모든 예제에서 여러 크레이트가 필요하다는 뜻이다.
또한 내 예제에서는 Cargo를 사용하지 않을 것이다. 내가 하는 일 중 Cargo를 사용할 때 보통 하는 방식과 다른 지점들(예를 들어 이 예제들에서 extern crate를 사용하는 점)은 따로 언급하려고 한다. 그리고 나중 글에서는 Cargo가 다양한 연결 시나리오를 현재 어떻게 처리하는지도 다뤄 보길 바란다. 하지만 지금은 아주 작은 걸음부터 가려 한다. 우선은 rustc 자체에만 집중하자.
아주 높은 수준에서 보면, 우리는 대략 다음과 같은 오브젝트 파일 구조를 보게 될 것이다:
graph LR SbSource[simple-bin.rs] SbObj["simple-bin (exec)"] SbSource --> SbCompile((rustc)) SbCompile -- generates --> SbObj
즉, simple-bin.rs 같은 입력이 주어지면, 그것을 rustc에 넣어 출력으로 실행 파일을 얻을 수 있다는 뜻이다.
위와 같은 경우의 실제 코드는 사소하다:
1 2 3``` // simple-bin.rs #![crate_type="bin"] // ("bin" is the default; other examples vary here.) fn main() { println!("Running main from {}", file!()); }
이 코드가 준비되면 프로그램을 컴파일하고 실행할 수 있다. (실제로는 개발자들이 보통 `cargo run`으로 이렇게 한다.)
1
2
3
4
5
6```
% rustc --out-dir out/bins/ps/dsb simple-bin.rs
% ls out/bins/ps/dsb
simple-bin
% out/bins/ps/dsb/simple-bin
Running main from simple-bin.rs
%
⊕ 나는 이 블로그 글에만 쓰이는 맞춤형 디렉터리 명명 규칙을 사용하고 있다. 예를 들어 이 호출의 --out-dir에서: 모든 출력은 out 아래로 간다. 실행 바이너리는 out/bin 아래로 간다. 빌드 중 정적 연결이 선호되었다면 out/bin/ps 아래로 간다(“prefer static”의 약자); 동적 연결이 선호되었다면 out/bin/pd 아래로 간다(“prefer dynamic”의 약자). 이 “선호”에 대해서는 아래에서 -Cprefer-dynamic을 설명하면서 더 다룬다. 마지막으로, 각 테스트마다 고유 키로 이름 붙인 디렉터리도 추가했다(여기서는 “demo-simple-bin”의 약자인 dsb). 그래서 ls 호출 결과가 깔끔하다. 이 예제들에서는 기본 출력 디렉터리를 덮어써서 각 예제가 자기만의 디렉터리를 갖게 할 것이다. 이렇게 하면 각 예제가 라이브러리를 정확히 어디서 가져오는지 명시해야 한다. 실제 내부에서 무슨 일이 일어나는지에 대한 이해를 다시 확인하는 좋은 방법이다.
lib 크레이트의 단순한 연결물론, 우리가 관심 있는 주제는 연결이므로, 라이브러리 크레이트가 관련된 예제들을 살펴보고 싶을 것이다. 아마도 그중 가장 단순한 예는 다음과 같다:
graph TD SlSource[simple-lib.rs] SlObj[libsimple_lib.rlib] SlSource --> SlCompile((rustc)) SlCompile --generates --> SlObj DemoSlCompile -.finds file.-> SlObj DemoSlSource[demo-simple-lib.rs] DemoSlObj[demo-simple-lib.obj] DemoSlSource --> DemoSlCompile((rustc)) SlObj --> link DemoSlCompile -- generates --> DemoSlObj subgraph "implicit link-step" DemoSlObj --> link link((link)) end link -- generates --> DemoSlBin["demo-simple-lib (exec)"]
왜 코드를 이렇게 분리해서, 나중에 연결되는 별도 라이브러리들로 나누는 것이 유리할까? 한 가지 이유는 어떤 코드가 활발히 개발 중인지 식별하고, 전체 제품과 분리해서 그 개발에 집중하기 위해서다. 따라서 simple-lib.rs가 개발 중이라면, 우리는 그것만 집중해서 다루고 demo-simple-lib.rs를 처리하는 데 도구의 시간을 쓰지 않아도 된다. 또는 반대로, demo-simple-lib.rs가 개발 중이라면 그것에만 집중할 수 있고, 도구는 많아야 simple-lib.rs에 rustc를 실행해 생성한 libsimple_lib.rlib 라이브러리만 처리하면 된다.
아래는 위 그림 중 demo-simple-lib를 컴파일하는 부분에 해당하는 코드와 명령들이다.
1 2 3 4 5 6 7``` // simple-lib.rs
// generate a library (what kind? The "default" for this platform) #![crate_type="lib"]
// Here's the function we'll provide to our clients. pub fn main() { println!("Running main from {}", file!()); }
1
2
3
4
5
6
7```
// demo-simple-lib.rs
// link to library built from simple-lib.rs
extern crate simple_lib;
// call function it exports
fn main() { simple_lib::main(); }
demo-simple-lib.rs에서는 #![crate_type="bin"]을 생략했는데, 그것이 기본 크레이트 타입이기 때문이다.
(실제로 현대의 대부분 Rust 코드는 extern crate를 사용하지 않는다. 대신 비슷한 효과를 내는 컴파일러 옵션 주입을 Cargo가 처리하게 둔다. 지금은 초기 예제에서 extern crate를 사용해서, Rust 코드 자체가 별도 라이브러리에 대한 의존성을 드러내도록 하겠다.)
위 두 파일이 준비되면 각각을 컴파일하고 결과 바이너리를 실행할 수 있다.
1 2 3 4 5 6 7 8 9``` % rustc --out-dir out/ps/sl simple-lib.rs % ls out/ps/sl libsimple_lib.rlib % rustc --out-dir out/bins/ps/dsl demo-simple-lib.rs -Lout/ps/sl % ls out/bins/ps/dsl demo-simple-lib % out/bins/ps/dsl/demo-simple-lib Running main from simple-lib.rs %
⊕ 도표를 찡그리며 보다가 그 도표가 설명하는 관계가 정말로 이 명령들이 하는 일을 제대로 반영하는지 어떻게 확인할 수 있을지 궁금하다면, 이 글 끝의 “누가 그 라이브러리를 사용하고 있는가?” 부록을 보길 권한다. 거기서는 크레이트들을 함께 연결할 때 생기는 몇 가지 문제를 단계별로 살펴본다.
첫 번째 `rustc` 호출, 즉 `simple-lib.rs`를 컴파일하는 과정은 `simple-bin.rs` 예제와 매우 비슷하다. 두 번째 `rustc` 호출, 즉 `demo-simple-lib.rs`를 컴파일하는 과정에는 새로운 점이 있다. `-Lout/sl` 옵션을 넘긴다는 점이다. 이것은 컴파일러에게 “외부 크레이트를 해석해야 한다면, 검색 경로 목록에 `out/sl`을 추가하라”는 뜻이다.
`demo-simple-lib` 예제는 Rust의 `lib` 크레이트 타입을 보여 주는데, 이것은 Rust가 지원하는 라이브러리 형식들 중 하나를 컴파일러가 정의하여 선택하는 형태로 명세되어 있다.
이 블로그 글 시점에서 Rust의 `lib` 크레이트 타입은 적어도 내 머신에서는 `rlib`에 매핑된다. `rlib`에 대해서는 아래에서 더 이야기하겠지만, 지금은 `simple-lib`에 대해 위에서 언급한 점을 기억하면 된다: 컴파일러 자신이 `rlib` 크레이트에 저장된 메타데이터를 읽고, _또한_ 링커 역시 `rlib` 크레이트에서 정의를 추출한다.
이제 다른 지원 크레이트 타입들을 차례로 살펴보고, 그것들이 어떻게 동작하는지 비슷한 방식으로 보여 주겠다.
⊕ 지금 전체 글을 다시 읽어 보니, `dylib`이 열어 놓는 토끼굴의 수를 생각하면 맨 끝에 두었어야 했다는 생각이 든다. 아마 나중에 이 글을 수정해서 순서를 바꿀지도 모르겠다. 그러면 사람들이 먼저 “쉬운 경우들”을 볼 수 있을 테니까.
## 동적 라이브러리: `dylib`
Rust 레퍼런스에 다음으로 나오는 크레이트 타입은 `dylib`이다.
graph TD SlSource[simple-dylib.rs] SlObj[libsimple_dylib.so] SlSource --> SlCompile((rustc)) SlCompile --generates --> SlObj DemoSlCompile -.finds file.-> SlObj DemoSlSource[demo-simple-dylib.rs] DemoSlObj[demo-simple-dylib.obj] DemoSlSource --> DemoSlCompile((rustc)) SlObj --> link DemoSlCompile -- generates --> DemoSlObj subgraph "implicit link-step" DemoSlObj --> link link((link)) end link -- generates --> DemoSlBin["demo-simple-dylib (exec)"] SlObj -. "dynamically loaded by" .- DemoSlBin
이 그림은 위에서 `lib`에 대해 제시한 [그림](https://blog.pnkfx.org/blog/2022/05/12/linking-rust-crates/#Simple.linkage.of-a..lib..crate)과 비슷하다. 실제로 두 그림을 나란히 놓고 보면 대부분의 차이는 일관된 이름 바꾸기로 설명할 수 있다 … 단 하나를 제외하면 말이다. 이제 도표에 추가 호가 하나 생겼고, `demo-simple-dylib` 실행 파일이 `libsimple_dylib.so` 라이브러리 파일과 직접 연결되어 있다.
이는 `dylib` 크레이트가 동적 라이브러리이기 때문이다. 즉, 프로그램이 실행될 때 _동적으로_ 로드되도록 의도된 것이다. 여기에 대한 이유는 몇 가지가 있다. 예를 들어 프로그램이 “플러그인” 아키텍처를 지원하도록 설계되었을 수 있다. 이 경우 다른 동적 라이브러리로 바꿔 끼움으로써 새로운 동작을 불러올 수 있다. 또 다른 흔한 이유는 실행 바이너리 크기를 줄이기 위해서다. 많은 프로그램이 같은 외부 크레이트에 의존한다면, 모두가 같은 `dylib`를 공유하는 편이 더 효율적일 수 있다.
_참고: `dylib` 형식은 Rust 컴파일러의 서로 다른 버전 사이에서 안정적으로 유지된다고 보장되지 않는다. 따라서 `dylib` 형식을 사용한다면, 같은 `dylib`를 공유하는 모든 크레이트와 그 `dylib` 자체가 모두 같은 버전의 Rust 컴파일러로 빌드되었음을 보장해야 한다._
아래는 위 그림 중 `demo-simple-dylib"`를 컴파일하는 부분에 해당하는 코드와 명령들이다.
1
2
3```
// simple-dylib.rs
#![crate_type="dylib"]
pub fn main() { println!("Running main from {}", file!()); }
1 2 3``` // demo-simple-dylib extern crate simple_dylib; fn main() { simple_dylib::main(); }
위 두 파일이 준비되면 각각을 컴파일하고 결과 바이너리를 실행할 수 있다.
1
2
3
4
5
6
7```
% rustc --out-dir out/pd/sd -C prefer-dynamic simple-dylib.rs
% ls out/pd/sd
libsimple_dylib.so
% rustc --out-dir out/bins/ps/dsd demo-simple-dylib.rs -Lout/pd/sd
% ls out/bins/ps/dsd
demo-simple-dylib
% LD_LIBRARY_PATH=out/pd/sd:$(rustc --print=sysroot)/lib out/bins/ps/dsd/demo-simple-dylib
demo-simple-dylib 코드가 demo-simple-lib 코드와 매우 비슷하다는 점을 눈치챘을지도 모른다. 실제로 소스에서 유효한 변화는 simple-dylib.rs의 crate_type 차이뿐이다. 반면 이 입력들을 컴파일하고 실행 파일을 실행하는 명령들은 꽤 큰 변화가 적용되었다. 왜 그런 변화가 필요했는지 살펴보자.
이 파일들을 컴파일하고 아마 생성되었을 실행 파일을 실행하기 위해 demo-simple-lib의 단계 순서를 그대로 재사용하려 하면, 몇 가지 장애물에 부딪히게 된다.
먼저 simple-dylib.rs를 simple-lib.rs에 사용했던 것과 같은 명령으로 컴파일해 보려 하면, 처음에는 되는 것처럼 보인다:
1 2 3 4 5``` % mkdir -p out/ps/sd % rustc --out-dir out/ps/sd simple-dylib.rs % ls out/ps/sd libsimple_dylib.so %
문제는 그렇게 컴파일된 생성 dylib를 _사용하려고_ 할 때 발생한다. 그 단계에서 다음과 같은 오류 출력이 나온다:
1
2
3
4
5
6
7
8
9
10```
```sh
% rustc --out-dir out/bins/ps/dsd demo-simple-dylib.rs -Lout/ps/sd
error: cannot satisfy dependencies so `std` only shows up once
|
= help: having upstream crates all available in one format will likely make this go away
error: cannot satisfy dependencies so `core` only shows up once
|
= help: having upstream crates all available in one format will likely make this go away
[...]
그리고 이와 비슷한 오류가 상위 크레이트 std, core, compiler_builtins, rustc_std_workspace_core, alloc, libc, unwind, cfg_if, hashbrown, rustc_std_workspace_alloc, std_detect, rustc_demangle, addr2line, gimli, object, memchr, miniz_oxide, adler, panic_unwind에 대해 계속 이어진다.
왜 이런 일이 벌어지는지 이해하려면 한 걸음 물러서야 한다.
명시적인 지시가 프로그래머에게서 주어지지 않은 경우, 컴파일러는 각 의존성을 생성되는 실행 바이너리에 어떻게 포함할지 결정해야 한다. 즉, 어떤 의존성을 출력 오브젝트에 “정적으로 연결”할 것인가(이는 사실상 오브젝트가 필요로 하는 것이 참조된 라이브러리에서 출력으로 복사되어 들어간다는 뜻이다), 아니면 그 의존성을 “동적으로 연결”할 것인가(이는 실행 파일이 런타임에 해석되어야 하는 동적 라이브러리에 대한 의존성을 지니게 된다는 뜻이다)를 결정해야 한다. 이는 사소하지 않은 결정인데, Rust는 개별 크레이트가 여러 서로 다른 크레이트 타입을 동시에 지원하도록 선택할 수 있게 하기 때문이다. 하나의 크레이트가 “나는 rlib나 dylib로 사용될 수 있다. 내 하위 클라이언트가 자기 필요에 맞는 오브젝트 파일을 선택하게 하겠다”고 말할 수 있다.
하지만 어떤 선택을 해야 하는지 아무도 컴파일러에 말해 주지 않으면, Rust 컴파일러는 어떤 옵션이 가장 적절할지 추정하기 위해 다소 단순한 전술을 적용한다. 그리고 현재 그 전술은 -C prefer-dynamic의 존재 여부에 크게 영향을 받는다.
simple-dylib.rs가 -C prefer-dynamic 없이 컴파일되었을 때, 컴파일러는 그 플래그의 부재를 simple-dylib.rs의 모든 의존성을 정적으로 연결하려 시도해야 한다는 신호로 해석했다.
더 나아가 컴파일러는 실제로 그런 의존성들의 정적 연결에 성공하지만, 그렇게 함으로써 결과적으로 정적으로 연결된 그 크레이트를 demo-simple-dylib.rs에 연결하는 것을 불가능하게 만들어 버린다.
⊕ 여기서 합리적인 사람이라면 이렇게 지적할 수 있다. “왜 demo-simple-dylib에서의 std 연결이 그 자체로 별개의 것으로 취급되는가? 다시 말해, 이미 그 크레이트들을 정적으로 연결한 simple-dylib가 그것들을 demo-simple-dylib에 제공하도록 컴파일러가 그냥 두면 안 되는가?” 그리고 그렇게 생각하는 사람이 당신뿐인 것도 아니다. 여기서 사용되는 기본 로직을 담당한 Alex Crichton도 rust-lang/rust#34909에서 같은 제안을 했다. 이 질문은 미래 글에서 더 탐구할 계획이다. demo-simple-dylib 크레이트 또한 같은 상위 크레이트들 모두에 연결하려고 하는데, 컴파일러는 simple-dylib와 demo-simple-dylib 양쪽이 그런 의존성들의 중복 사본을 가지는 것은 합법적이지 않다고 말하며 이를 거부한다.
⊕ 여기서 설명한 의미론은 Rust가 아직 1.0에 도달하기도 전인 2014년에 도입된 RFC 404까지 거슬러 올라간다. 그 RFC 자체는 코드 주석을 문서로 가리키고 있는데, 컴파일러가 여러 차례 리팩터링을 거치면서 그 주석의 위치는 옮겨졌다. 하지만 가장 최신 버전은 rustc_metadata::dependency_format에서 찾을 수 있다.
-Cprefer-dynamic과 --extern 플래그가 없고(또한 모든 것을 정적으로 연결하도록 강제하는 어떤 제약도 없는) 상황에서, 어떤 상위 크레이트 타입을 사용할지 결정하려는 컴파일러의 기본 로직은 다음과 같다:
먼저 모든 상위 의존성을 .rlib 라이브러리를 통해 정적으로 연결해 본다. 성공하면 끝이다.
정적 연결이 실패했다면, 일반적으로 연결 자체가 불가능하거나, 적어도 하나의 의존성은 동적 라이브러리여야 한다. 적어도 하나가 동적이어야 하므로, 컴파일러는 단순한 전술로 가능한 한 많은 상위 의존성을 그 dylib 오브젝트 파일들로 만족시키려 한다.
하지만 -Cprefer-dynamic을 제공하면, 컴파일러에게 정적 연결 단계를 시도하지 말고 대신 가능한 경우 상위 의존성에 대해 dylib를 선호하라고 지시하게 된다. 그리고 이것이 여기서 필요한 수정이다. 즉, simple-lib.dylib가 std의 dylib 버전을 선호하도록 해야 한다.
1``` % rustc --out-dir out/pd/sd -C prefer-dynamic simple-dylib.rs
그것이 갖춰지면 나머지는 자연스럽게 따라온다.
먼저 `demo-simple-dylib.rs`가 어떻게 빌드되는지 보자.
1```
% rustc --out-dir out/bins/ps/dsd demo-simple-dylib.rs -Lout/pd/sd
⊕ 여기 이야기가 단순한 이유 중 하나는 simple-dylib.rs가 단 하나의 크레이트 타입으로만 컴파일되었기 때문이다. 만약 그것에 대해 dylib와 rlib 둘 다 생성했다면, 그때는 demo-simple-dylib를 빌드할 때 -Cprefer-dynamic의 존재 여부가 중요해진다.
일단 simple-dylib 크레이트가 dylib로만 제공된다는 사실이 확정되면, demo-simple-dylib를 빌드할 때 -Cprefer-dynamic을 넘기든 아니든 상관없다. 빼면 컴파일러는 먼저 모든 의존성을 정적으로 연결하려 시도할 뿐이다. 그리고 simple-dylib 때문에 그것이 불가능하다고 판단하면, 가능한 한 많이 동적 라이브러리를 사용하려는 시도로 되돌아간다. 따라서 simple-dylib와 std 둘 다 동적 라이브러리로 해석하게 된다.
다음으로 demo-simple-dylib를 실행하는 방법을 생각해 보자:
1``` % LD_LIBRARY_PATH=out/pd/sd:$(rustc --print=sysroot)/lib out/bins/ps/dsd/demo-simple-dylib
여기서 핵심은 `LD_LIBRARY_PATH`에 항목을 추가해야 했다는 점이다. 프로그램이 실행될 때 상위 `dylib` 의존성을 만족시켜야 하는데, 우리는 방금 그것들이 `simple-dylib`(위치는 `out/pd/sd`)와 `std`(위치는 `rustc --print=sysroot`를 실행해 `rustc` 자신에게 그 지원 파일들이 어디 사는지 물어서 얻은 디렉터리)라는 점을 확인했다.
## Rust 라이브러리: `rlib`
휴, 이건 꽤 지쳤다.
다음에는 더 쉬운 경우를 살펴보자. Rust 라이브러리 타입 `rlib`이다.
이것이 반드시 단순한 경우는 아니지만, 다루기엔 분명한 경우다. 왜냐하면 우리는 내내 이것을 이야기하고 있었기 때문이다. 단지 그렇게 말하지 않았을 뿐이다.
구체적으로 말하면 `demo-simple-lib` 예제는 Rust의 `lib` 크레이트 타입을 다루었는데, 이것은 Rust가 지원하는 라이브러리 종류 중에서 컴파일러가 정의하여 선택하는 것으로 명세되어 있다. 이 블로그 글 시점에서 내 머신에서는 Rust의 `lib` 크레이트 타입이 `rlib`에 매핑된다.
따라서 위에서 `lib`에 대해 설명한 사용 패턴과 문제들은 모두 `rlib`에도 그대로 적용된다.
완전성을 위해 `rlib`가 어떻게 사용되는지 보여 주는 도표를 하나 실어 둔다. 매우 익숙해 보일 것이다.
graph TD SlSource[simple-rlib.rs] SlObj[libsimple_rlib.rlib] SlSource --> SlCompile((rustc)) SlCompile --generates --> SlObj DemoSlCompile -.finds file.-> SlObj DemoSlSource[demo-simple-rlib.rs] DemoSlObj[demo-simple-rlib.obj] DemoSlSource --> DemoSlCompile((rustc)) SlObj --> link DemoSlCompile -- generates --> DemoSlObj subgraph "implicit link-step" DemoSlObj --> link link((link)) end link -- generates --> DemoSlBin["demo-simple-rlib (exec)"]
마찬가지로, 아래는 도표에 나온 파일들의 소스 코드다.
1
2
3```
// simple-rlib.rs
#![crate_type="rlib"]
pub fn main() { println!("Running main from {}", file!()); }
1 2 3``` // demo-simple-rlib.rs extern crate simple_rlib; fn main() { simple_rlib::main(); }
마지막으로 위 도표를 구현하는 명령 호출들이다.
1
2
3
4
5
6
7
8
9```
% rustc --out-dir out/ps/sr simple-rlib.rs
% ls out/ps/sr
libsimple_rlib.rlib
% rustc --out-dir out/bins/ps/dsr demo-simple-rlib.rs -Lout/ps/sr
% ls out/bins/ps/dsr
demo-simple-rlib
% out/bins/ps/dsr/demo-simple-rlib
Running main from simple-rlib.rs
%
하지만 여기에는 놀랄 만한 점이 없다.
staticlib다른 언어에서 Rust를 호출하고 싶다면, Rust 코드를 정적 라이브러리로 컴파일한 다음 그 정적 라이브러리를 외부 프로그램에서 연결함으로써 그렇게 할 수 있다.
이 예제의 시연 목적상 나는 Rust를 사용해 demo-simple-staticlib를 구현하고 있다. 하지만 중요한 점은, 그럴 필요가 없었다는 것이다. 그것은 C로 작성할 수도 있었고, Java와 JNI를 사용해 인터페이스할 수도 있었다. (simple-staticlib가 Python 확장일 수도 있고, 등등.)
staticlib의 연결 그림은 다음과 같다:
graph TD SlSource[simple-staticlib.rs] SlObj[libsimple_staticlib.a] SlSource --> SlCompile((rustc)) SlCompile --generates --> SlObj %% DemoSlCompile -.finds file.-> SlObj DemoSlSource[demo-simple-staticlib.rs] DemoSlObj[demo-simple-staticlib.obj] DemoSlSource --> DemoSlCompile((rustc)) SlObj --> link DemoSlCompile -- generates --> DemoSlObj subgraph "implicit link-step" DemoSlObj --> link link((link)) end link -- generates --> DemoSlBin["demo-simple-staticlib (exec)"]
이 그림을 rlib 그림과 비교하면, 이제 가장 큰 차이는 더 이상 rustc에서 libsimple_staticlib.a로 가는 호가 없다는 점이다. 이것은 사실 꽤 큰 차이다!
생성된 아카이브 파일 libsimple_staticlib.a는 Rust 크레이트가 아니다. Rust 컴파일러가 이를 크레이트로 해석하는 데 필요한 메타데이터를 갖고 있지 않다. 대신 이것은 C 프로젝트에서 보통 사용하는 다른 라이브러리 아카이브와 같은, 그저 또 하나의 라이브러리 아카이브일 뿐이다.
demo-simple-staticlib.rs를 컴파일할 때는, 그 정적 라이브러리에 연결하라고 컴파일러에 알려 주는 플래그를 넘겨야 한다. 컴파일러가 더 이상 이것을 마법처럼 알아서 처리해 주지 않는다.
libsimple_staticlib.a는 Rust 크레이트가 아니므로, demo-simple-staticlib.rs 안에서 그것이 제공하는 함수들에 대한 명시적 선언을 제공해야 한다.
위 차이들은 이렇게 요약할 수도 있다: 함수 시그니처를 일치시키고 링커 플래그를 다루어야 한다는 점에서, 마치 C로 프로그래밍하는 것과 같다. 그런 경험이 있다면 전혀 놀랍지 않을 것이다.
그렇지만, 아래는 도표의 파일들에 대한 소스 코드다.
1 2 3 4``` // simple-staticlib.rs #![crate_type="staticlib"] #[no_mangle] pub extern "C" fn staticlib_main() { println!("Running staticlib_main from {}", file!()); }
1
2
3
4```
fn main() {
extern "C" { fn staticlib_main(); }
unsafe { staticlib_main(); }
}
그리고 아래는 위 도표를 완성하는 명령 호출들이다.
1 2 3 4 5 6 7 8 9``` % rustc --out-dir out/ps/ss simple-staticlib.rs % ls out/ps/ss libsimple_staticlib.a % rustc --out-dir out/bins/ps/dss demo-simple-staticlib.rs -Lout/ps/ss -lsimple_staticlib % ls out/bins/ps/dss demo-simple-staticlib % out/bins/ps/dss/demo-simple-staticlib Running staticlib_main from simple-staticlib.rs %
## 비 Rust 코드용 동적 라이브러리: `cdylib`
개념적으로 지금까지 우리는 아래 2x2 행렬의 세 칸을 다루었고, `cdylib`가 표를 완성한다.
| | Rust에서 연결 | 비 Rust에서 연결 |
| --- | --- | --- |
| **정적** | rlib | staticlib |
| **동적** | dylib | cdylib |
`cdylib`를 사용하는 도표에는 `dylib` 표와 `staticlib` 표를 모두 떠올리게 하는 요소가 들어 있다.
graph TD SlSource[simple-cdylib.rs] SlObj[libsimple_cdylib.so] SlSource --> SlCompile((rustc)) SlCompile --generates --> SlObj %% DemoSlCompile -.finds file.-> SlObj DemoSlSource[demo-simple-cdylib.rs] DemoSlObj[demo-simple-cdylib.obj] DemoSlSource --> DemoSlCompile((rustc)) SlObj --> link DemoSlCompile -- generates --> DemoSlObj subgraph "implicit link-step" DemoSlObj --> link link((link)) end link -- generates --> DemoSlBin["demo-simple-cdylib (exec)"] SlObj -. "dynamically loaded by" .- DemoSlBin
즉, 여기서 우리는 다음을 본다:
1. `demo-simple-staticlib`와 매우 비슷하게, `demo-simple-cdylib.rs`를 컴파일할 때는 `simple-cdylib.so`에서 메타데이터를 추출하거나 그것을 Rust 크레이트로 취급하려고 하지 않는다. 대신 올바른 링커 플래그를 컴파일러에 제공하고, `demo-simple-cdylib.rs` 소스 코드에 올바른 `extern` 함수 시그니처를 제공해야 한다.
2. `demo-simple-dylib`와 매우 비슷하게, `demo-simple-cdylib`의 실행은 공유 라이브러리 `demo-simple-cdylib.so`를 직접 로드하고 그 코드에 동적으로 연결한다.
이 시연의 소스 코드는 `staticlib`와 똑같다.
1
2
3
4
5
6```
// simple-cdylib.rs
#![crate_type="cdylib"]
#[no_mangle]
pub extern "C" fn cdylib_main() {
println!("Running cdylib_main from {}", file!());
}
1 2 3 4 5``` // demo-simple-cdylib.rs fn main() { extern "C" { fn cdylib_main(); } unsafe { cdylib_main(); } }
여기서의 명령 시퀀스는 흥미롭다. 우리는 더 이상 `-C prefer-dynamic`을 강제로 사용할 필요가 없다.
1
2
3
4
5
6
7
8```
% rustc --out-dir out/ps/sc simple-cdylib.rs
% ls out/ps/sc
libsimple_cdylib.so
% rustc --out-dir out/bins/ps/dsc demo-simple-cdylib.rs -Lout/ps/sc -lsimple_cdylib
% ls out/bins/ps/dsc
demo-simple-cdylib
% LD_LIBRARY_PATH=out/ps/sc out/bins/ps/dsc/demo-simple-cdylib
Running cdylib_main from simple-cdylib.rs
⊕ 이 가설은 미래 글에서 후속으로 다루고 싶은 여러 항목 중 하나다. 내 생각에는 이는 두 구성 요소(simple-cdylib.so와 demo-simple-dylib)를 완전히 분리된 개체로 취급하게 되기 때문인 듯하다. 따라서 둘은 Rust 표준 라이브러리에서 사용하는 함수들의 자기만의 사본 을 각각 정적으로 연결해 넣게 된다.
꽤 정신없는 여정이었다!
그런데도 우리는 아직 더 복잡한 것들에는 도달하지도 못했다. 예를 들면:
--extern으로 지정하는 법,-Cprefer-dynamic을 사용한 크레이트와 사용하지 않은 크레이트를 섞는 일(오늘날 이것을 동작하게 만드는 방법, 그리고 이상적인 세계에서는 어떻게 동작해야 하는가?), 또는게다가 나는 정적 연결이 무엇을 의미하는지 도 실제로 깊이 파고들지 않았다. 특히 rlib처럼 명시적으로 링크 시점 의존성이 없는 크레이트 타입에 대해서는 더 그렇다. 이것도 가능하다면 생성된 코드에 대해 objdump를 사용해 무엇을 배울 수 있는지 보여 주면서 더 자세히 설명하고 싶다.
따라서 미래 글을 위한 소재는 많다.
(그리고 이 글의 표현을 더 깔끔하게 다듬어 볼 기회도 많다.)
다음 도표를 보자:
graph TD SlObj[libsimple_lib.rlib] DemoSlCompile -.finds file.-> SlObj DemoSlSource[demo-simple-lib.rs] DemoSlObj[demo-simple-lib.obj] DemoSlSource --> DemoSlCompile((rustc)) SlObj --> link DemoSlCompile -- generates --> DemoSlObj subgraph "implicit link-step" DemoSlObj --> link link((link)) end
여기서 스스로에게 던져야 할 중요한 질문이 있다. libsimple_lib.rlib는 실제로 링커에 의해 사용되는가? 아니면 오직 rustc에 대한 입력으로만 사용되는가(즉, 잠재적으로는 demo-simple-lib.obj 자체를 생성하는 데만 쓰이는가)? 아니면 rustc와 링커 둘 다 에 의해 사용되는가?
우리는 명령을 약간만 바꿔 이 질문을 직접 시험해 볼 수 있다.
먼저 컴파일러 단계의 나머지 부분과 링크 호출을 분리한 다음, 그것을 수정해서 독립적으로 실행함으로써 링커가 그것을 실제로 사용하는지 시험해 볼 수 있다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16``` % mkdir -p out/sl out/bins/ps/dsl2step % rustc --out-dir out/ps/sl simple-lib.rs % ls out/ps/sl libsimple_lib.rlib % LINKER_COMMAND=$(rustc --out-dir out/bins/ps/dsl2step demo-simple-lib.rs -Lout/ps/sl -Csave-temps --print=link-args -Ccodegen-units=1) % ls out/bins/ps/dsl2step demo-simple-lib demo-simple-lib.demo_simple_lib.66aa33f3-cgu.0.rcgu.o demo-simple-lib.demo_simple_lib.66aa33f3-cgu.0.rcgu.bc demo-simple-lib.hlsxcd1s0pxo20a.rcgu.bc demo-simple-lib.demo_simple_lib.66aa33f3-cgu.0.rcgu.no-opt.bc demo-simple-lib.hlsxcd1s0pxo20a.rcgu.o % ./out/bins/ps/dsl2step/demo-simple-lib Running main from simple-lib.rs % rm ./out/bins/ps/dsl2step/demo-simple-lib % eval $LINKER_COMMAND % ./out/bins/ps/dsl2step/demo-simple-lib Running main from simple-lib.rs %
위 시퀀스의 의미는 다음과 같다. `rustc -Csave-temps --print=link-args`를 실행하면 생성된 오브젝트 파일들을 보존하고, 실제로 실행한 링커 호출도 출력한다.
⊕ 실제로 이런 종류의 실험을 할 때는, 내가 여기서 보여 준 방식처럼 `eval`을 무턱대고 사용하지 말고, 먼저 화면에 링커 명령을 출력해서 실행해도 되는 신뢰할 수 있는 것인지 확인해야 한다. 생성된 바이너리를 실행하고, 그 바이너리를 삭제한 다음, 그 링커 명령을 직접 다시 실행할 수 있다(`eval $LINKER_COMMAND`). 그리고 바이너리를 다시 실행한다. 이로써 그 링커 호출이 실제로 그 바이너리를 생성한다는 점을 확인할 수 있다.
이 준비가 되면, 링커의 파일 사용을 시험하는 한 가지 방법은 단순히 `libsimple_lib.rlib`를 삭제한 뒤 원래 링커 호출을 실행하는 것이다:
1
2
3
4
5
6```
% eval $LINKER_COMMAND
% rm out/ps/sl/libsimple_lib.rlib
% ls out/ps/sl/
% eval $LINKER_COMMAND
/usr/bin/ld: cannot find /media/pnkfelix/Rust/Linking/out/ps/sl/libsimple_lib.rlib: No such file or directory
collect2: error: ld returned 1 exit status
우리 오브젝트 코드가 그 파일에 의존한다는 사실을 드러내는 또 다른, 조금 더 복잡한 방법도 있다. 링커 호출에서 libsimple_lib.rlib에 대한 참조를 제거한 뒤, 새로 만들어진 링커 호출을 실행하는 것이다:
1
2
3
4
5```
% NEW_COMMAND=$(echo "$LINKER_COMMAND" | sed -e 's@"-Wl,-Bstatic" .*/libsimple_lib.rlib"@@')
% eval $NEW_COMMAND
/usr/bin/ld: out/bins/ps/dsl2step/demo-simple-lib.demo_simple_lib.66aa33f3-cgu.0.rcgu.o: in function demo_simple_lib::main': demo_simple_lib.66aa33f3-cgu.0:(.text._ZN15demo_simple_lib4main17h9794b393d760d697E+0x3): undefined reference to simple_lib::main'
collect2: error: ld returned 1 exit status
이것은 빌드 산출물을 가지고 장난치기 시작할 때 맞닥뜨리게 되는 불쾌한 오류 메시지들이 어떤 종류인지 감을 준다. 링커는 정당하게 `demo_simple_lib::main`에 대한 생성 오브젝트 코드가 `simple_lib::main`을 참조하고 있다고 불평하고 있다(실제로 전자의 정의는 후자를 한 번 호출하는 것뿐이다). 그런데 그 참조를 만족시킬 수 없다는 것이다. (사용자는 이 메시지를 읽고 핵심 문제는 링커가 더 이상 `libsimple_lib.rlib`의 경로를 명령줄 인수 중 하나로 받지 않는다는 점임을 추론해야 한다.)
### 컴파일러의 라이브러리 의존성 증명하기
이전 절에서는 링커가 `.rlib` 파일을 필요로 한다는 점을 확립했다. 하지만 어쩌면 그것은 링크 단계에만 필요한 것이고, `rustc` 자체는 실제 파일을 필요로 하지 않는 것일 수도 있지 않을까?
이 이론도 빌드 산출물에 대해 비슷한 직접 실험 형태로 시험해 볼 수 있다.
먼저 파일을 제거해 보자(이전 절에서 보인 것과 같다)
1
2
3
4
5
6
7
8
9
10
11
12
13```
% rm -f out/ps/sl/libsimple_lib.rlib
% ls out/ps/sl
% rustc --out-dir out/bins/ps/dsl demo-simple-lib.rs -Lout/ps/sl
error[E0463]: can't find crate for `simple_lib`
--> demo-simple-lib.rs:1:1
|
1 | extern crate simple_lib;
| ^^^^^^^^^^^^^^^^^^^^^^^^ can't find crate
error: aborting due to previous error
For more information about this error, try `rustc --explain E0463`.
%
이것은 컴파일러가 demo-simple-lib.rs를 컴파일하는 과정에서 libsimple_lib.rlib 파일을 분명히 사용하고 있음을 보여 준다.
컴파일러가 억지로라도 앞으로 나아가게 하려고 더미 파일을 주더라도 여전히 불평할 것이다:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16```
% touch out/ps/sl/libsimple_lib.rlib
% ls -s out/ps/sl/
total 0
0 libsimple_lib.rlib
% rustc --out-dir out/bins/ps/dsl demo-simple-lib.rs -Lout/ps/sl
error[E0786]: found invalid metadata files for crate simple_lib
--> demo-simple-lib.rs:1:1
|
1 | extern crate simple_lib;
| ^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: failed to mmap file '/media/pnkfelix/Rust/Linking/out/ps/sl/libsimple_lib.rlib': memory map must have a non-zero length
error: aborting due to previous error
For more information about this error, try rustc --explain E0786.
곰곰이 생각해 보면 이 모든 것은 완전히 타당하다. `demo-simple-lib.rs`를 컴파일하는 과정의 일부로서, 컴파일러는 외부 메타데이터(즉 `simple-lib` 크레이트의 함수 시그니처와 타입 정의)를 참조하게 될 것이기 때문이다.