리눅스 시스템 프로그래밍

  • 시스템 프로그래밍의 핵심은 system call이다
  • 응용 프로그램은 주로 system call을 통해 커널에 다양한 서비스를 요청한다
  • system call은 메모리 안전성, 보안 문제 등 다양한 문제를 동반한다
  • 러스트는 메모리 안전성을 중점으로 설계된 언어로, 시스템 프로그래밍에서 장점이 부각된다

FFI (Foreign Function Interface)

  • FFI는 한 프로그래밍 언어가, 다른 언어로 작성된 함수나 라이브러리를 호출하는 매커니즘이다
  • 이는 언어 사이의 코드 재사용과 성능 최적화에 큰 이점을 제공한다
  • 러스트는 이를 통해서 C/C++로 작성된 라이브러리를 쉽게 호출할 수 있다
  • 그러나 문제가 발생 시 디버깅이 복잡해질 수 있어, 필요한 부분에 제한적으로 사용하는 것이 좋다

Bindgen을 활용해 C코드 호출

c, header 파일

# hello.h
void hello(const char* msg);

# hello.c
#include <stdio.h>

void hello(const char* msg) {
    printf("from rust: %s\n", msg);
}
  • 위와 같은 c 코드가 있다고 하자
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++ 코드 호출

cpp, header 파일

#pragma once

#include <iostream>

class Test
{
public:
    Test(/* args */);
    ~Test();
    void say_hello();
};

int add(int a, int b);
  • 테스트를 위한 test.h 파일과
#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;
}
  • test.cpp 파일을 추가하자

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());
    }
}
  • libc의 printf를 사용할 수 있다

0개의 댓글