일반적으로 시스템 라이브러리라함은 네이티브 언어로 작성되었음을 의미한다. 그리고 보통 네이티브 언어는 c/c++과 같은 언어를 의미한다. 시스템 라이브러리는 다른 언어로 작성한 프로그램에서도 이용할 수 있어야 한다. 그래서 많은 프로그래밍 언어들이 c/c++로 작성된 라이브러리를 사용하기 위한 외부 함수 인터페이스(Foreign Function Interface) 패키지(이하 ffi 패키지)를 제공한다. java의 jni (java native interface), python의 ctype, ruby와 javascript의 ffi (foreign function interface) 등이 바로 그 것이다. 물론 rust에서도 c 라이브러리를 사용하기 위해 ffi 패키지를 제공한다.

그런데 rust는 c/c++과 마찬가지로 (다른 용도로도 사용할 수 있지만 기본적으로는) 시스템 프로그래밍을 위한 언어다. 따라서 rust로 작성된 라이브러리도 다른 언어에서 사용할 수 있어야 한다. 이를 위해 rust는 c 라이브러리와 같은 구조의 동적 라이브러리를 만들 수 있는 옵션을 제공한다.

동적 라이브러리 만들기

rust는 cargo라는 패키지 매니저가 많은 것들을 대신 해주기 때문에 c 라이브러리를 만드는 것과는 비교도 할 수 없을 정도로 편하다. c로 소스 코드를 작성하고 소스 코드만큼 많은 양의 makefile을 작성하던 과거의 나는 이제 안녕!

…은 개뿔. 프로그램이 커지고, 그래서 모듈이 많아지고, 따라서 디펜던시가 증가하고, 그로 인해 옵션이 많이지면 cargo도 복잡하긴 매한가지다. 너무 좋아하진 말자.

언어를 배운다는 것은 Hello, world! 를 출력하는 방법을 배우는 것이다. 그러니까 Hello, world! 를 출력하는 rust 라이브러리를 만들어 보자. 싫다면 다른 라이브러리를 알아서 만들어 보길 추천한다. 예를 들어 알파고알파고 라거나 알파고 같은 것 말이다.

일단 cargo를 이용해 새 패키지를 생성한다.

$ cargo new hello
$ cd hello
$ tree
.
├── Cargo.toml
└── src
   └── lib.rs

우리의 요구사항은 Hello, world! 를 출력하는 것이다. 요구 사항을 만족하는 소스 코드를 lib.rs 파일에 입력한다.

fn hello() {
   println!("Hello, world!");
}
$ cargo build
	Compiling hello v0.1.0 (file:///Users/arzhna/dev/hello)
src/lib.rs:1:1: 3:2 warning: function is never used: `hello`, #[warn(dead_code)] on by default
src/lib.rs:1 fn hello() {
src/lib.rs:2     println!("Hello, world!");
src/lib.rs:3 }

그대로 빌드하니 dead_code 어쩌고 하는 무시무시한 경고 메시지를 출력하며 컴파일이 됐다. 경고 메시지는 뭔지 잘 모르겠으니 무시하고, 라이브러리가 잘 만들어졌나 확인해보았다.

$ ls target/debug/lib*
target/debug/libhello.rlib

이럴 수가! 확장자가 rlib이다. rlib은 rust 정적 라이브러리 파일의 확장자다. ffi 패키지들은 so또는 dylib이라는 확장자를 가진 동적 라이브러리를 지원한다. python에서 제공하는 ctype의 c 라이브러리 로드 클래스는 이름부터 cdll(c dynamic link library)이다. rlib은 못 쓴다는 뜻이다.

살려야 한다! 동적 라이브러리를 만들어야 한다!

동적 라이브러리를 만들기 위해서는 동적 라이브러리를 만들기 위한 옵션을 설정해야 한다. Cargo.toml파일을 열고 적당한 위치에 아래와 같이 이 패키지는 동적 라이브러리여야만 해!라고 명시하기 위한 [lib]섹션을 추가하자.

[lib]
crate-type = ["dylib"]

그리고 다시 빌드해보자.

$ cargo build
	Compiling hello v0.1.0 (file:///Users/arzhna/dev/hello)
src/lib.rs:1:1: 3:2 warning: function is never used: `hello`, #[warn(dead_code)] on by default
src/lib.rs:1 fn hello() {
src/lib.rs:2     println!("Hello, world!");
src/lib.rs:3 }
$
$ ls target/debug/lib*
target/debug/libhello.dylib

(역시 경고는 무시하고) 확인해보니 이번에는 dylib이라는 확장자를 가진 동적 라이브러리가 잘 생성되었다. 만세!

그렇다면 이대로 쓰면 될까? 경고가 찝찝한데?

메시지를 다시 살펴보니 function is never used 라고 경고하고 있다. hello() 함수를 어디서도 사용하고 있지 않고 있기 때문이다. 아무도 사용하지 않는 dead_code 가 있는 경우 경고 메시지를 발생시키는 것이 rust 컴파일러의 기본 설정이다.

라이브러리가 제공하는 API 함수라면 라이브러리 내에서 사용하고 있지 않아도 전혀 이상할 것이 없는데 왜 경고 메시지가 나올까? 혹시 API 함수가 아니라면? 그렇다. 처음에 작성했던 hello() 함수는 로컬 함수였다. 외부에서 사용할 수 있도록 선언되지 않았기 때문에 경고를 발생시킨 것이다. 로컬 함수를 API 함수로 바꾸기 위해서는 몇가지 어노테이션을 추가해야 한다.

pub extern fn hello() {
   println!("Hello, world!");
}

pubextern이라는 어노테이션이 추가되었다. pub은 public 함수임을 선언하기 위해, extern은 외부에서도 사용할 수 있음을 선언하기 위해 붙이는 어노테이션이다.

이제 외부에서 사용할 수 있는 동적 라이브러리를 만들 수 있을까? 아직이다. 대부분의 컴파일러는 컴파일 과정에서 네임 맹글링(name mangling)이라는 작업을 수행한다. 정의되어 있는 함수명을 정해진 규칙에 의해 변경하는 작업이다. 그런데 불행히도 언어마다 맹글링 규칙이 다르다. c와 c++을 혼용해 사용하는 프로그램에서는 extern "C" {...}와 같은 코드들을 흔하게 볼 수 있다. 이는 c와 c++의 맹글링 규칙이 다르기 때문에 c 스타일로 맞춰주기 위한 코드다.

c 라이브러리를 로드하는 패키지들은 c 스타일의 맹글링만을 고려한다. 따라서 rust 컴파일러가 c 스타일의 맹글링을 하도록 해야한다. 다음과 같이 #[no_mangle]어노테이션을 추가해 rust 컴파일러의 맹글링 스위치를 꺼버리자.

#[no_mangle]
pub extern fn hello() {
   println!("Hello, world!");
}

이제 다른 언어에서 사용할 수 있는 동적 라이브러리를 생성할 준비가 끝났다. 빌드하자.

$ cargo build
  Compiling hello v0.1.0 (file:///Users/arzhna/dev/test/hello)
$ ls target/debug/lib*          
target/debug/libhello.dylib

경고 메시지도 없고 동적 라이브러리도 제대로 생성되었다. 신난다!

that's easy

참 쉽죠?

rust 동적 라이브러리 사용하기

서두에서 언급했던 바와 같이 대부분의 언어들은 c/c++ 라이브러리를 사용하기 위한 패키지를 제공한다. 이전 섹션에서 만든 rust 라이브러리는 c 스타일로 컴파일된 동적 라이브러리다. 따라서 c/c++ 라이브러리를 사용하기 위한 패키지를 그대로 이용할 수 있다.

python

파이썬은 ctype이라는 패키지를 제공한다. ctype의 cdll이라는 클래스를 통해 c/c++ 라이브러리를 로드할 수 있다. 백문이 불여일견! 바로 테스트해보자.

# hello.py

from ctypes import cdll

# libhello 라이브러리를 로드한다.
lib = cdll.LoadLibrary("./target/release/libhello.dylib")

# libhello 라이브러리가 제공하는 hello() api를 호출한다.
lib.hello()

print("done!")

실행하면?

$ python hello.py
Hello, world!
done!
$

굿잡!

c

c에서 rust 라이브러리를 사용하기 위해서는 헤더 파일이 필요하다. rust 라이브러리가 제공하는 API들의 프로토타입을 c 스타일로 선언해주면 오케이!

// hello.h

void hello(void);

헤더 파일이 생겼으니 이제 소스 코드를 작성하고, c 라이브러리 링크하던 대로 옵션 주고, 그렇게 하던 대로 빌드하면 된다.

// hello.c

#include <stdio.h>
#include "hello.h"

int main(){
   hello();
   return 0;
}
$ gcc -lhello -L./target/release/ -I. hello.c -o hello
$ ./hello
Hello, world!

성공!

swift

swift는 objective-c를 기반으로 만들어져 있다. 당연히 c와 호환성이 우수하다. rust로 작성된 라이브러리지만 c 라이브러리와 다를바 없으므로 Bridging-Header만 만들면 그대로 사용할 수 있다.

java

자바는 jni를 만들면되는데… 귀찮으니 패스하자.