본문 바로가기

Languages/Rust

dart ffi with rust

image credit: https://medium.com/flutter-community/how-to-call-a-rust-function-from-dart-using-ffi-f48f3ea3af2c

dart ffi(foreign function interface)를 통해 rust library를 사용하는 방법을 공유합니다. 예제 app으로는 rust의 audio library의 하나인 rodio 사용해서 음악을 재생합니다. 

본 글의 내용은 https://medium.com/flutter-community/how-to-call-a-rust-function-from-dart-using-ffi-f48f3ea3af2c 에서 영감을 얻어 작성하였고,  2022년 9월 기준으로 빌드 가능한 환경으로 구성했으며, rust library에 대한 내용을 추가했습니다.

 

준비물

  • dart SDK v2.18.0
  • rust v1.63.0
  • rodio에서 재생가능한 음악 파일 (wav, mp3, ...)
  • windows or macos (여기에서는 macOS를 사용했습니다.)

 

진행단계

  1. rust library 작성
  2. rust app 작성
  3. rust library의 ffi interface 적용
  4. rust library를 사용하는 dart app 작성

 

1. rust library 작성

dart app을 먼서 생성합니다. app 구현은 4단계에서 진행합니다.

# ~/examples/dart_audio_app_with_rust를 예제코드의 root(EX_ROOT)로 지정합니다.
cd ~/examples
dart create -t console dart_audio_app_with_rust
cd dart_audio_app_with_rust
export EX_ROOT=~/examples/dart_audio_app_with_rust

 

cargo rust library 생성합니다.

cargo new --lib audio_lib
cd audio_lib

 

rodio를 추가합니다.

cargo add rodio

 

library code(src/lib.rs)를 test code와 함께 작성합니다.

use rodio::{Decoder, OutputStream, Source};
use std::ffi::CStr;
use std::os::raw::c_char;
use std::{fs::File, io::BufReader};

pub fn play(path: &str) {
    // Get a output stream handle to the default physical sound device
    let (_stream, stream_handle) = OutputStream::try_default().unwrap();
    // Load a sound from a file, using a path relative to Cargo.toml
    let file = BufReader::new(File::open(path).unwrap());
    // Decode that sound file into a source
    let source = Decoder::new(file).unwrap();
    // Play the sound directly on the device
    stream_handle
        .play_raw(source.convert_samples())
        .expect("can't play audio file");

    // The sound plays in a separate audio thread,
    // so we need to keep the main thread alive while it's playing.
    std::thread::sleep(std::time::Duration::from_secs(1));
    println!("play sound in rust");
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_play_audio() {
        play("../data/beep-01a.wav");
    }
}

 

test를 실행하면 음악이 재생됩니다. (음악 파일 경로는 준비된 파일로 수정하세요.)

cargo test

 

2. rust app 작성

이번단계에서는 rust app에서 작성된 library 코드를 실행해 봅니다.

아래와 같이 app을 생성합니다.

cd $EX_ROOT
cargo new audio_app
cd audio_app

 

app의 Cargo.toml에 audio_lib 의존성을 추가합니다.

[dependencies]
audio_lib = { path = "../audio_lib" }

 

application code(src/main.rs)를 작성합니다.

use audio_lib::play;

fn main() {
    play("data/beep-01a.wav");
}

 

아래와 같이 실행합니다. 1단계에서 재생한 음악이 그대로 나오면 app도 잘 작동하는 것입니다.

cargo run

 

3. rust library의 ffi interface 적용

이 단계에서는 rust library를 외부에서 쓸 수 있도록 dynamic library로 빌드하고 api도 c interface로 wrapping 합니다.

c type의 dynamic library로 빌드되도록 audio_lib/Cargo.toml 수정합니다.

[lib]
crate-type = ["cdylib", "lib"]

 

library 코드의 c용 interface를 추가합니다.

#[no_mangle]
pub extern "C" fn play_for_ffi(ptr: *const c_char) {
    let cstr_path = unsafe { CStr::from_ptr(ptr) };
    play(cstr_path.to_str().unwrap());
}

이제 rust library를 C기반 application에서 사용할 수 있는단계가 되었습니다.

 

4. rust library를 사용하는 dart app 작성

dart application code를 아래와 같이 작성합니다.

import 'dart:ffi' as ffi;
import 'dart:ffi';
import 'package:ffi/ffi.dart';

// rust/native function
typedef NativePlayFunction = ffi.Void Function(ffi.Pointer<Utf8>);
// dart function
typedef PlayFunction = void Function(ffi.Pointer<Utf8>);

void main(List<String> arguments) {
  ffi.DynamicLibrary dl =
      ffi.DynamicLibrary.open('target/debug/libaudio_lib.dylib');
  final PlayFunction play = dl
      .lookup<ffi.NativeFunction<NativePlayFunction>>("play_for_ffi")
      .asFunction();
  final ffi.Pointer<Utf8> path = "data/beep-01a.wav".toNativeUtf8().cast();
  play(path);
}

 

dart app에서 필요한 의존성 패키지를 설치합니다.

dart pub add ffi

 

이제 아래를 실행하면 dart 에서 rust library를 호출하여 audio가 재생되는 것을 들을 수 있습니다.

dart run

 

위에서 소개한 내용은 https://github.com/yeoupooh/dart_audio_app_with_rust 에서 전체 소스가 공개되어 있습니다.

다음엔 flutter 에서 rust library를 사용하는 예제를 만들어 보겠습니다.