리눅스 시스템 프로그래밍
- 시스템 프로그래밍의 핵심은 system call이다
- 응용 프로그램은 주로 system call을 통해 커널에 다양한 서비스를 요청한다
- system call은 메모리 안전성, 보안 문제 등 다양한 문제를 동반한다
- 러스트는 메모리 안전성을 중점으로 설계된 언어로, 시스템 프로그래밍에서 장점이 부각된다
FFI (Foreign Function Interface)
- FFI는 한 프로그래밍 언어가, 다른 언어로 작성된 함수나 라이브러리를 호출하는 매커니즘이다
- 이는 언어 사이의 코드 재사용과 성능 최적화에 큰 이점을 제공한다
- 러스트는 이를 통해서 C/C++로 작성된 라이브러리를 쉽게 호출할 수 있다
- 그러나 문제가 발생 시 디버깅이 복잡해질 수 있어, 필요한 부분에 제한적으로 사용하는 것이 좋다
Bindgen을 활용해 C코드 호출
# hello.h
void hello(const char* msg);
# hello.c
#include <stdio.h>
void hello(const char* msg) {
printf("from rust: %s\n", msg);
}
sijin@Sijin:~/rust$ gcc -c c_src/hello.c
sijin@Sijin:~/rust$ gcc -shared -o libhello.so hello.o
- 그리고 c 코드를 shared object 형태로 만들어보자
Cargo.toml
[build-dependencies]
bindgen = "0.71.0"
- 이 c코드 함수를 호출하기 위해서 Cargo.toml에 bindgen 빌드 의존성을 추가하자
- 이전까지 사용한 dependencies가 아닌 build-dependencies에 추가해야 한다
build.rs
# build.rs
use std::{env, path::PathBuf};
use bindgen::builder;
extern crate bindgen;
fn main() {
println!("cargo:rustc-link-search=."); // 현재 디렉토리를 link directory로 설정
println!("cargo:rustc-link-lib=hello"); // hello 라이브러리 링킹
println!("gargo:rerun-if-changed=c_src/hello.h"); // c_src/hello.h 파일 변경 시 다시 빌드
// c_src/hello.h에 대한 러스트 바인딩을 생성
let bindings = builder()
.header("c_src/hello.h")
.parse_callbacks(Box::new(bindgen::CargoCallbacks::new()))
.generate()
.expect("Unable to generate bindings");
let out_path = PathBuf::from(env::var("OUT_DIR").unwrap());
// 생성된 바인딩을 bindings.rs에 작성
bindings
.write_to_file(out_path.join("bindings.rs"))
.expect("couldn't write bindings");
}
- 프로젝트 루트 폴더에 build.rs를 생성한다
- 러스트에서 build.rs 파일은 컴파일 전에 실행되는 빌드 스크립트 역할이다
- build.rs는 일반 Rust 실행 코드(src/main.rs의 main 함수)와는 완전히 별개로 동작한다
- build.rs는 c_str/hello.h를 읽어 rust binding 파일을 생성한다
/* automatically generated by rust-bindgen 0.71.1 */
unsafe extern "C" {
pub fn hello(msg: *const ::std::os::raw::c_char);
}
- 자동으로 생성된 rust binding은 위와 같다
rust에서 호출
use std::ffi::CString;
mod bindings {
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
}
fn main() {
let c_to_print = CString::new("hello rust").expect("CString::new failed");
unsafe {
bindings::hello(c_to_print.as_ptr());
}
}
- 메인 코드를 작성하자
- include! 매크로를 사용해 build.rs가 생성한 bindings.rs를 로드한다
- 문자열 처리를 위해 CString을 사용한다
- CString은 '\0'으로 끝나는 C 언어 형태의 문자열을 만들어준다
- bindings 모듈에서 생성된 hello 함수는 unsafe한 C코드에서 작성되었으므로, unsafe 블록으로 감싼다
sijin@Sijin:~/rust$ LD_LIBRARY_PATH=. cargo run
Compiling calc v0.1.0 (/home/sijin/rust)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.07s
Running `target/debug`
from rust: hello rust
- libhello.so를 찾을 수 있도록 LD_LIBRARY를 현재 디렉토리로 설정하여 프로그램을 실행하면
- c언어 함수가 실행된 것을 볼 수 있다
autocxx를 사용해 C++ 코드 호출
#pragma once
#include <iostream>
class Test
{
public:
Test(/* args */);
~Test();
void say_hello();
};
int add(int a, int b);
#include "test.h"
Test::Test(/* args */)
{
std::cout << "Test created" << std::endl;
}
Test::~Test()
{
std::cout << "Test deleted" << std::endl;
}
void Test::say_hello()
{
std::cout << "hello" << std::endl;
}
int add(int a, int b)
{
return a + b;
}
Cargo.toml
[dependencies]
cxx = "*"
autocxx = "*"
[build-dependencies]
autocxx-build = "*"
miette = {version = "*", features = ["fancy"]}
- autocss를 사용하기 위해서는 cxx, autocxx 의존성과 autocxx-build, miette 빌드 의존성을 추가해야한다
build.rs
fn main() -> miette::Result<()> {
let path = std::path::PathBuf::from("src");
let mut builder = autocxx_build::Builder::new("src/main.rs", [&path])
.build()
.expect("build failed");
builder
.flag_if_supported("-std=c++14")
.file("src/test.cpp")
.compile("autocxx-demo");
println!("cargo::rerun-if-changed=src/main.rs");
println!("cargo::rerun-if-changed=src/input.h");
Ok(())
}
- build.rs에서 miette 라이브러리를 사용해서 빌드 방식을 지정한다
rust에서 호출
use autocxx::prelude::*;
include_cpp!(
#include "test.h" // test.h 파일 포함
safety!(unsafe_ffi) // ffi 호출이 안전하지 않음을 명시
generate!("Test") // Test 클래스와
generate!("add") // add 함수를 러스트에 노출
);
fn main() {
println!("{:?}", ffi::add(autocxx::c_int(1), autocxx::c_int(2)));
let mut test = ffi::Test::new().within_box();
test.as_mut().say_hello();
}
sijin@Sijin:~/rust$ cargo run
...
c_int(3)
Test created
hello
Test deleted
include_cpp! 매크로로 input.h 파일을 불러올 수 있고
- 이를 통해 헤더파일에 정의된 클래스, 함수들을 러스트에서 직접 사용할 수 있다
unsafe
- unsafe는 컴파일러에게 해당 블록 코드는 안전하지 않다고 알려줄 때 사용한다
- 이는 러스트의 안전 검사기를 무시한다
- 시스템 api를 호출하거나 메모리를 직접 조작하는 경우 unsafe 키워드를 사용해야 한다
libc 연동
- libc로 표준 c 라이브러리의 입출력, 문자열, 메모리 할당 등 기능을 사용할 수 있다
[dependencies]
libc = "0.2"
- Cargo.toml에 libc 의존성을 추가하자
use std::ffi::CString;
fn main() {
let msg = CString::new("hello rust\n").expect("CString::new failed");
unsafe {
libc::printf(msg.as_ptr());
}
}