Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

WIT 리소스 사용하기(Rust)

리소스는 컴포넌트 외부(즉, 호스트나 다른 컴포넌트)에 존재하는 엔터티에 대한 핸들입니다.

스택 기반 역폴란드 표기법(RPN) 계산기 예제

이 섹션에서 예제로 사용할 리소스는 역폴란드 표기법(RPN) 계산기입니다. (어느 정도 연배가 있는 엔지니어라면 1970년대 휴대용 계산기에서 이를 기억할 것입니다.)

RPN 계산기는 상태를 가진 엔터티입니다. 사용자는 계산기 내부에서 유지되는 스택에 피연산자와 연산을 푸시한 다음, 스택을 평가하여 값을 산출합니다.

WIT에서 이 리소스는 다음과 같습니다:

package docs:rpn@0.1.0;

interface types {
    enum operation {
        add,
        sub,
        mul,
        div
    }

    resource engine {
        constructor();
        push-operand: func(operand: u32);
        push-operation: func(operation: operation);
        execute: func() -> u32;
    }
}

world calculator {
    export types;
}

컴포넌트에서 리소스 구현 및 내보내기

Rust로 계산기를 구현하려면 다음과 같이 하십시오:

  1. 라이브러리 컴포넌트를 생성하고, 위의 WIT를 wit/world.wit로 저장한 다음, wit-bindgen을 추가하십시오:

    cargo add wit-bindgen
    

    Cargo.toml에 cdylib 크레이트 타입도 반드시 포함하십시오:

    [lib]
    crate-type = ["cdylib"]
    
  2. 계산기 상태를 나타내는 Rust struct를 정의하십시오:

    #![allow(unused)]
    fn main() {
    use std::cell::RefCell;
    
    struct CalcEngine {
        stack: RefCell<Vec<u32>>,
    }
    }

    스택을 RefCell로 감싸는 이유는 무엇입니까? 뒤에서 보겠지만, 계산기 엔진에 대해 생성된 Rust 트레이트는 self에 대한 불변 참조를 가집니다. 하지만 이 트레이트를 구현할 때는 스택을 변경해야 합니다. 따라서 RefCell<T>나 Arc<RwLock<T>>와 같이 내부 가변성을 허용하는 타입이 필요합니다.

  3. 내보낸 리소스에 대해 생성된 바인딩(bindings.rs)에는 GuestX라는 이름의 트레이트가 포함되며, 여기서 X는 리소스 이름입니다. 계산기 engine 리소스의 경우 이 트레이트는 GuestEngine입니다. 2단계에서 정의한 struct에 이 트레이트를 구현하십시오:

    #![allow(unused)]
    fn main() {
    mod bindings {
        wit_bindgen::generate!();
    }
    
    use bindings::exports::docs::rpn::types::{Guest, GuestEngine, Operation};
    
    impl GuestEngine for CalcEngine {
        fn new() -> Self {
            CalcEngine {
                stack: RefCell::new(vec![])
            }
        }
    
        fn push_operand(&self, operand: u32) {
            self.stack.borrow_mut().push(operand);
        }
    
        fn push_operation(&self, operation: Operation) {
            let mut stack = self.stack.borrow_mut();
            let right = stack.pop().unwrap(); // TODO: error handling!
            let left = stack.pop().unwrap();
            let result = match operation {
                Operation::Add => left + right,
                Operation::Sub => left - right,
                Operation::Mul => left * right,
                Operation::Div => left / right,
            };
            stack.push(result);
        }
    
        fn execute(&self) -> u32 {
            self.stack.borrow_mut().pop().unwrap() // TODO: error handling!
        }
    }
    }
  4. 이제 engine 계약을 구현하는 동작하는 계산기 타입이 있지만, 이 타입을 engine 리소스 타입에 연결하는 작업이 아직 남아 있습니다. 이는 생성된 Guest 트레이트를 구현하여 수행합니다. 이 WIT의 경우 Guest 트레이트에는 연관 타입 외에는 아무것도 포함되어 있지 않습니다. 빈 struct를 사용하여 Guest 트레이트를 구현할 수 있습니다. 리소스에 대한 연관 타입(이 예제에서는 Engine)을 리소스 트레이트를 구현하는 타입(이 예제에서는 GuestEngine을 구현하는 CalcEngine struct)으로 설정하십시오. 그런 다음 export! 매크로를 사용하여 매핑을 익스포트합니다:

    #![allow(unused)]
    fn main() {
    struct Component;
    
    impl Guest for Component {
        type Engine = CalcEngine;
    }
    
    bindings::export!(Component);
    }

이것으로 계산기 engine 리소스의 구현이 완료됩니다. cargo build --target=wasm32-wasip2를 실행하여 컴포넌트 .wasm 파일을 생성하십시오.

컴포넌트에서 리소스 가져오기 및 사용하기

다른 컴포넌트에서 계산기 엔진을 사용하려면 해당 컴포넌트가 리소스를 임포트해야 합니다.

  1. 이전 섹션에서 설명한 대로 실행 가능한 컴포넌트를 생성하십시오.

  2. 프로젝트에 wit/component.wit를 추가하고 RPN 계산기 타입을 임포트하는 WIT 월드를 작성하십시오:

    package docs:rpn-cmd;
    
    world app {
        import docs:rpn/types@0.1.0;
    }
    
  3. docs:rpn(engine 리소스를 포함함)에 대한 관련 WIT 파일을 가져올 수 있도록 wkg.toml 파일을 생성하십시오:

    [overrides]
    "docs:rpn" = { path = "../path/to/docs-rpn/wit" }
    

    이 작업을 마친 후 wkg wit fetch를 실행하여 모든 WIT를 로컬에서 사용할 수 있도록 할 수 있습니다.

  4. 이제 리소스는 생성된 바인딩에서 적절한 연관 함수를 갖춘 struct로 나타납니다. 이를 사용하여 테스트 앱을 구성하십시오.

    mod bindings {
        wit_bindgen::generate!({
            generate_all,
        });
    }
    
    use bindings::docs::rpn::types::{Engine, Operation};
    
    fn main() {
        let calc = Engine::new();
        calc.push_operand(1);
        calc.push_operand(2);
        calc.push_operation(Operation::Add);
        let sum = calc.execute();
        println!("{sum}");
    }

컴포넌트를 있는 그대로 빌드하면 충족되지 않은 임포트, 즉 docs:rpn/types 임포트가 있는 WebAssembly 컴포넌트가 생성됩니다.

컴포넌트를 빌드한 후에는 리소스를 구현하는 .wasm 컴포넌트와 합성해야 합니다.. 합성을 통해 충족되지 않은 임포트가 없는 컴포넌트가 생성되면, 합성된 command 컴포넌트를 wasmtime run으로 실행할 수 있습니다.

또는 docs:rpn/types 임포트(및 관련 리소스)를 제공할 수 있는 호스트를 사용하여 컴포넌트를 “불완전한” 상태로 실행할 수도 있습니다(호스트가 예상되는 임포트를 제공하여 컴포넌트를 “완성“하기 때문입니다).

호스트에서 리소스 구현을 구현하고 익스포트하기

Wasm 런타임을 호스팅하는 경우, 게스트가 사용할 수 있도록 호스트에서 리소스를 익스포트할 수 있습니다.

런타임 호스팅은 이 책의 범위를 벗어나므로 여기서는 대략적인 개요만 제공합니다. 이는 Wasmtime 런타임에 한정된 내용이며, 다른 런타임에서는 다르게 표현될 수 있습니다.

  1. wasmtime::component::bindgen!을 사용하여 호스트로서 제공할 WIT를 지정하십시오.

    #![allow(unused)]
    fn main() {
    wasmtime::component::bindgen!({
        path: "../wit"
    });
    }
  2. with 필드를 통해 호스트에서 리소스를 어떻게 표현할지 bindgen!에 알려 주십시오. 이는 어떤 Rust 타입이든 될 수 있습니다. 예를 들어 RPN 엔진은 CalcEngine 구조체로 표현할 수 있습니다.

    #![allow(unused)]
    fn main() {
    wasmtime::component::bindgen!({
        path: "../wit",
        with: {
            "docs:rpn/types/engine": CalcEngine,
        }
    });
    }

    리소스의 호스트 표현을 지정하지 않으면 기본값으로 빈 열거형이 사용됩니다. 리소스는 대개 상태를 가지므로 이는 거의 유용하지 않습니다.

  3. 표현 타입이 내장 타입이 아니라면 이를 정의하십시오.

    #![allow(unused)]
    fn main() {
    struct CalcEngine { /* ... */ }
    }
  4. 호스트로서 이미 Host 트레이트를 구현하고 있을 것입니다. 이제 Host 트레이트와 동일한 타입에 HostX 트레이트(여기서 X는 리소스 이름)를 구현해야 합니다.

    #![allow(unused)]
    fn main() {
    impl docs::rpn::types::HostEngine for MyHost {
        fn new(
            &mut self,
        ) -> wasmtime::Result<wasmtime::component::Resource<docs::rpn::types::Engine>> {
            /* ... */
        }
    
        fn push_operand(
            &mut self,
            self_: wasmtime::component::Resource<docs::rpn::types::Engine>,
            operand: u32,
        ) -> wasmtime::Result<()> {
            /* ... */
        }
    
        // etc.
    }
    }

    중요: 이 트레이트는 리소스 표현이 아니라 ‘전체’ 호스트 타입에 구현합니다! 따라서 이 함수들의 self 참조는 ‘전체’ 호스트 타입을 가리킵니다. 리소스의 인스턴스 메서드의 경우, 인스턴스는 wasmtime::component::Resource 타입의 두 번째 매개변수(self_)로 식별됩니다.

  5. 호스트에 wasmtime::component::ResourceTable을 추가하십시오.

    #![allow(unused)]
    fn main() {
    struct MyHost {
        calcs: wasmtime::component::ResourceTable,
    }
    }
  6. 리소스 메서드 구현에서 이 테이블을 사용하여 리소스 표현의 인스턴스를 저장하고 접근하십시오.

    #![allow(unused)]
    fn main() {
    impl docs::rpn::types::HostEngine for MyHost {
        fn new(
            &mut self,
        ) -> wasmtime::Result<wasmtime::component::Resource<docs::rpn::types::Engine>> {
            self.calcs.push(CalcEngine::new())
        }
    
        fn push_operand(
            &mut self,
            self_: wasmtime::component::Resource<docs::rpn::types::Engine>,
            operand: u32,
        ) -> wasmtime::Result<()> {
            let calc_engine = self.calcs.get(&self_)?;
            // calc_engine is a CalcEngine - call its functions
            calc_engine.push_operand(operand);
            Ok(())
        }
    
        // etc.
    }
    }

원문 © 2023 The Bytecode Alliance Contributors · WebAssembly Components Documentation · CC BY 4.0
Yeokja 한국어 번역·편집 · Anthropic 사의 claude-sonnet-5 모델을 활용하여 번역되었으며 학습을 모두 비허용한 상태로 작업하였습니다 · 이 번역은 원문을 수정한 문서이며 CC BY 4.0으로 제공합니다.