Wippy에서 Rust 실행

Rust WebAssembly 컴포넌트를 빌드하고 Wippy에 등록한 뒤 함수, CLI, HTTP 엔트리로 노출합니다.

분류: 외부 Rust 컴포넌트 도구 체인을 사용하는 실행 가능한 튜토리얼. WIT, Rust 구현, Wippy 레지스트리, 무결성 해시 워크플로, 명령, 예상 결과, 실패 확인 절차를 제공합니다.

만들 컴포넌트

내보낸 함수 네 개를 갖는 Rust 컴포넌트를 만듭니다.

  • greet — 이름을 받아 인사말을 반환합니다.
  • add — 두 정수를 더합니다.
  • fibonacci — n번째 피보나치 수를 계산합니다.
  • list-files — 마운트된 디렉터리의 파일을 나열합니다.

Wippy 애플리케이션은 이 내보내기를 호출 가능한 함수, CLI 명령, HTTP 엔드포인트로 등록합니다.

사전 요구 사항

  • Wippy 런타임 v0.3.32a
  • wasm32-wasip1 대상이 설치된 Rust 도구 체인
  • 작동하는 C 도구 체인. Linux에서 cargo-component는 OpenSSL 개발 라이브러리도 필요로 합니다.
  • 이 튜토리얼에서 사용하는 cargo-component 0.21.1
rustup target add wasm32-wasip1
cargo install cargo-component --version 0.21.1 --locked

생성되는 컴포넌트 골격과 Wippy 디렉터리를 만듭니다.

mkdir rust-wasm-demo
cd rust-wasm-demo
cargo component new --lib demo
mkdir -p app/src/demo/wasm

PowerShell에서는 다음 명령을 사용합니다.

New-Item -ItemType Directory -Path rust-wasm-demo
Set-Location rust-wasm-demo
cargo component new --lib demo
New-Item -ItemType Directory -Path app\src\demo\wasm -Force

cargo component new는 호환되는 Cargo.toml, src/lib.rs, WIT 파일을 만들고 이후 src/bindings.rs를 다시 생성합니다. 생성된 wit-bindgen-rt 버전을 설치된 cargo-component와 짝지어 유지하세요. 도구는 이 인터페이스를 실험적이라고 설명하며 버전 간 생성 코드 호환성을 보장하지 않습니다.

프로젝트 구조

rust-wasm-demo/
├── demo/                    # Rust component
│   ├── Cargo.toml
│   ├── wit/
│   │   └── world.wit       # WIT interface
│   └── src/
│       ├── bindings.rs      # generated by cargo-component
│       └── lib.rs           # implementation
└── app/                     # Wippy application
    ├── wippy.lock
    └── src/
        ├── _index.yaml      # Infrastructure
        └── demo/
            ├── _index.yaml  # CLI processes
            └── wasm/
                ├── _index.yaml          # WASM entries
                └── demo_component.wasm  # Compiled binary

1단계: WIT 인터페이스 생성

WebAssembly Interface Types(WIT)는 호스트와 게스트 컴포넌트 사이의 계약을 정의합니다.

demo/wit/world.wit를 만듭니다.

package component:demo;

world demo {
    export greet: func(name: string) -> string;
    export add: func(a: s32, b: s32) -> s32;
    export fibonacci: func(n: u32) -> u64;
    export list-files: func(path: string) -> string;
}

각 내보내기는 Wippy에서 호출할 수 있는 함수가 됩니다.

2단계: Rust로 구현

생성된 demo/Cargo.toml을 유지합니다. 패키지 메타데이터는 WIT 패키지와 일치하는 component:demo를 대상으로 해야 하며, 라이브러리 크레이트 유형은 cdylib로 유지해야 합니다.

demo/src/lib.rs를 만듭니다.

#[allow(warnings)]
mod bindings;

use bindings::Guest;

struct Component;

impl Guest for Component {
    fn greet(name: String) -> String {
        format!("Hello, {}!", name)
    }

    fn add(a: i32, b: i32) -> i32 {
        a + b
    }

    fn fibonacci(n: u32) -> u64 {
        if n <= 1 {
            return n as u64;
        }
        let (mut a, mut b) = (0u64, 1u64);
        for _ in 2..=n {
            let next = a + b;
            a = b;
            b = next;
        }
        b
    }

    fn list_files(path: String) -> String {
        let mut result = String::new();
        match std::fs::read_dir(&path) {
            Ok(entries) => {
                for entry in entries {
                    match entry {
                        Ok(e) => {
                            let name = e.file_name().to_string_lossy().to_string();
                            let meta = e.metadata();
                            let (kind, size) = match meta {
                                Ok(m) => {
                                    let kind = if m.is_dir() { "dir" } else { "file" };
                                    (kind, m.len())
                                }
                                Err(_) => ("?", 0),
                            };
                            let line = format!("{:<6} {:>8}  {}", kind, size, name);
                            println!("{}", line);
                            result.push_str(&line);
                            result.push('\n');
                        }
                        Err(e) => {
                            let line = format!("error: {}", e);
                            eprintln!("{}", line);
                            result.push_str(&line);
                            result.push('\n');
                        }
                    }
                }
            }
            Err(e) => {
                let line = format!("cannot read {}: {}", path, e);
                eprintln!("{}", line);
                result.push_str(&line);
                result.push('\n');
            }
        }
        result
    }
}

bindings::export!(Component with_types_in bindings);

bindings 모듈은 WIT 정의에서 cargo-component가 생성합니다.

3단계: 컴포넌트 빌드

cd demo
cargo component build --release

이 명령은 target/wasm32-wasip1/release/demo.wasm을 만듭니다. Wippy 애플리케이션으로 복사합니다.

mkdir -p ../app/src/demo/wasm
cp target/wasm32-wasip1/release/demo.wasm ../app/src/demo/wasm/demo_component.wasm

PowerShell에서는 다음 명령을 사용합니다.

New-Item -ItemType Directory -Path ..\app\src\demo\wasm -Force
Copy-Item -LiteralPath target\wasm32-wasip1\release\demo.wasm `
  -Destination ..\app\src\demo\wasm\demo_component.wasm

무결성 검증에 사용할 SHA-256 해시를 가져옵니다.

sha256sum ../app/src/demo/wasm/demo_component.wasm

PowerShell에서는 다음 명령을 사용합니다.

(Get-FileHash ..\app\src\demo\wasm\demo_component.wasm -Algorithm SHA256).Hash.ToLowerInvariant()

아래의 모든 YOUR_HASH_HERE에 소문자 16진수 문자 64개를 복사합니다. 최종 필드는 sha256:<64-hex-characters> 형식이어야 합니다. Rust 소스나 원래 빌드 경로가 아니라 복사된 바이너리의 해시입니다.

4단계: Wippy 애플리케이션

인프라

app/src/_index.yaml을 만듭니다.

version: "1.0"
namespace: demo

entries:
  - name: gateway
    kind: http.service
    meta:
      comment: HTTP server
    addr: ":8090"
    lifecycle:
      auto_start: true

  - name: api
    kind: http.router
    meta:
      comment: Public API router
      server: demo:gateway
    prefix: /

  - name: processes
    kind: process.host
    lifecycle:
      auto_start: true

  - name: terminal
    kind: terminal.host
    lifecycle:
      auto_start: true

  - name: policy
    kind: security.policy
    meta:
      comment: Grants access to mounted filesystems and WASM functions
    policy:
      actions:
        - fs.get
        - funcs.call
      resources: "*"
      effect: allow

파일시스템을 WASM 모듈에 마운트하는 것과 WASM 함수를 호출하는 것은 모두 보호되는 액션입니다. 이 정책이 두 액션을 허용하며, 이를 필요로 하는 엔트리가 정책을 참조합니다.

WASM 함수

app/src/demo/wasm/_index.yaml을 만듭니다.

version: "1.0"
namespace: demo.wasm

entries:
  - name: assets
    kind: fs.directory
    meta:
      comment: Filesystem with WASM binaries
    directory: ./src/demo/wasm

  - name: greet_function
    kind: function.wasm
    meta:
      comment: Greet function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: greet
    pool:
      type: inline

  - name: add_function
    kind: function.wasm
    meta:
      comment: Add function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: add
    pool:
      type: inline

  - name: fibonacci_function
    kind: function.wasm
    meta:
      comment: Fibonacci function via payload transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: fibonacci
    pool:
      type: inline

핵심 사항:

  • 하나의 fs.directory 엔트리가 WASM 바이너리를 제공합니다.
  • 여러 함수가 서로 다른 method 값으로 같은 바이너리를 참조합니다.
  • hash 필드는 로드 시 바이너리 무결성을 확인합니다.
  • inline 풀은 하나의 준비된 인스턴스를 통해 호출을 직렬화합니다. 동기 호출 사이에 호출별 실행 상태를 초기화합니다. 동시 워커가 필요하면 다른 풀 유형을 사용하세요.

WASI를 사용하는 함수

list-files 함수는 파일 시스템에 접근하므로 WASI 가져오기가 필요합니다.

  - name: list_files_function
    kind: function.wasm
    meta:
      comment: Filesystem listing with WASI mounts
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: list-files
    imports:
      - wasi:cli
      - wasi:io
      - wasi:clocks
      - wasi:filesystem
    wasi:
      mounts:
        - fs: demo.wasm:assets
          guest: /data
    pool:
      type: inline

wasi.mounts 섹션은 Wippy 파일 시스템 엔트리를 게스트 경로에 매핑합니다. WASM 모듈 안에서 /data는 demo.wasm:assets 디렉터리를 가리킵니다.

CLI 명령

app/src/demo/_index.yaml을 만듭니다.

version: "1.0"
namespace: demo.cli

entries:
  - name: wasm_cli_policy
    kind: security.policy
    policy:
      actions:
        - fs.get
      resources:
        - demo.wasm:assets
      effect: allow

  - name: ls
    kind: process.wasm
    meta:
      comment: List files from mounted WASI filesystem
      command:
        name: ls
        short: List files from mounted directory
        security:
          actor: {id: demo.cli:ls}
          policies: [demo:policy]
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: list-files
    imports:
      - wasi:cli
      - wasi:io
      - wasi:clocks
      - wasi:filesystem
    wasi:
      mounts:
        - fs: demo.wasm:assets
          guest: /data

meta.command 블록은 프로세스를 이름이 있는 CLI 명령으로 등록합니다. greet 명령은 문자열 연산만 사용하므로 WASI 임포트가 필요 없습니다. ls 명령은 파일시스템 접근이 필요하므로, 마운트를 허용하는 보안 컨텍스트도 함께 지닙니다.

HTTP 엔드포인트

app/src/demo/wasm/_index.yaml에 다음을 추가합니다.

  - name: http_greet
    kind: function.wasm
    meta:
      comment: Greet exposed via wasi-http transport
    fs: demo.wasm:assets
    path: /demo_component.wasm
    hash: sha256:YOUR_HASH_HERE
    method: greet
    transport: wasi-http
    pool:
      type: inline

  - name: http_greet_endpoint
    kind: http.endpoint
    meta:
      comment: HTTP POST endpoint for WASM greet
      router: demo:api
    method: POST
    path: /greet
    func: http_greet

wasi-http 전송은 HTTP 요청/응답 컨텍스트를 WASM 인수와 결과에 매핑합니다.

5단계: 초기화 및 실행

cd app
wippy init

CLI 명령 실행

# List available commands
wippy run list
Available commands:

  greet  Greet someone via WASM  (demo.cli:greet)
  ls  List files from mounted directory  (demo.cli:ls)

Run with: wippy run <command>

명령 이름 뒤의 인자는 익스포트된 함수에 문자열 파라미터로 전달되므로, 각 명령은 WIT 시그니처가 선언한 인자를 정확히 받습니다:

# Run greet
wippy run greet World
Hello, World!
# Run ls to list mounted directory
wippy run ls /data

명령은 최소한 demo_component.wasm과 파일 크기를 출력하고 상태 0으로 종료되어야 합니다. Wippy는 임의의 process.wasm 반환 페이로드를 출력하지 않으므로 CLI 예제에서는 WASI stdout에 쓰는 Rust 함수를 사용합니다.

서비스로 실행

wippy run

포트 8090에서 HTTP 서버가 시작됩니다. wasi-http 트랜스포트는 요청 본문을 함수의 단일 문자열 인자로 전달합니다:

curl -X POST http://localhost:8090/greet -d 'World'
Hello, World!

Lua에서 호출

WASM 함수는 Lua 함수와 동일한 방식으로 호출됩니다. 호출하는 프로세스에는 대상에 대한 funcs.call 권한이 필요하며, demo:policy가 이를 부여합니다:

local funcs = require("funcs")

local greeting, err = funcs.call("demo.wasm:greet_function", "World")
-- greeting: "Hello, World!"

local sum, err = funcs.call("demo.wasm:add_function", 6, 7)
-- sum: 13

local fib, err = funcs.call("demo.wasm:fibonacci_function", 10)
-- fib: 55

문제 해결과 정리

  • cargo component를 알 수 없다는 오류가 나오면 설치한 뒤 cargo component build를 다시 실행하세요. 이 구성에서 일반 cargo build는 같은 바인딩이나 컴포넌트 출력을 생성하지 않습니다.
  • 첫 빌드 전에 src/bindings.rs가 없는 것은 정상입니다. cargo component build 후에도 파일이 없다면 WIT 패키지 또는 컴포넌트 메타데이터를 해석하지 못한 것입니다. 바이너리를 복사하기 전에 해당 빌드 오류를 해결하세요.
  • WASM hash mismatch는 문서화된 다이제스트를 계산한 뒤 바이너리가 변경되었거나 자리표시자가 남아 있다는 뜻입니다. 릴리스 바이너리를 다시 복사하고 다이제스트를 다시 계산한 뒤 참조하는 모든 엔트리를 업데이트하세요.
  • 가져오기 인스턴스화 오류는 컴포넌트가 엔트리에서 생략한 호스트 프로필을 가져온다는 뜻입니다. 파일 시스템 예제에는 문서화된 wasi:cli, wasi:io, wasi:clocks, wasi:filesystem 가져오기를 유지하세요.
  • cannot read /data는 wasi.mounts 게스트 경로나 해당 파일 시스템 엔트리가 레지스트리와 일치하지 않는다는 뜻입니다.
  • Ctrl+C로 HTTP 런타임을 중지합니다. Rust 빌드 출력은 demo/target/ 아래에 남습니다. 생성된 산출물을 정리하려면 이 디렉터리와 복사된 .wasm 파일을 제거하세요.

다음 단계