순수 Ruby로 구현된 WebAssembly 런타임, Roite

[EN] Running ruby.wasm on Pure Ruby Wasm Runtime / Uchio KONDO @udzura

작성자
RubyKaigi
발행일
2025년 05월 27일

핵심 요약

  • 1 Roite는 순수 Ruby로 작성된 WebAssembly 런타임으로, Ruby 애플리케이션 내에서 Wasm 모듈 실행을 가능하게 하며 높은 이식성과 유지보수성을 제공합니다.
  • 2 Wasm 코어 스펙 및 WASI(WebAssembly System Interface) 지원을 통해 파일 시스템 접근 등 복잡한 애플리케이션 실행을 구현, Ruby 자체를 Roite 위에서 구동하는 데 성공했습니다.
  • 3 성능 최적화를 위해 블록 점프 계산 캐싱, 객체 인스턴스 생성 최소화 등을 적용했으며, 특히 YJIT 사용 시 최대 57%의 실행 시간 단축 효과를 확인했습니다.

도입

Roite는 순수 Ruby로 개발된 WebAssembly(Wasm) 런타임으로, Ruby 애플리케이션 내에서 Wasm 모듈을 직접 실행할 수 있도록 설계되었습니다. 이 프로젝트는 Ruby의 표준 라이브러리에만 의존하며 외부 C 의존성이나 Gem 없이 높은 이식성과 유지보수성을 목표로 합니다. 발표자는 Roite가 CRuby, JRuby 등 다양한 Ruby 구현체에서 작동할 수 있도록 순수 Ruby로 Wasm 엔진을 구축함으로써 Ruby와 Wasm 간의 통합을 확장하고 Wasm의 언어 불가지론적 특성과 이식성을 Ruby 생태계에 활용하고자 하는 비전을 제시합니다.

Roite 개발은 여러 이정표를 거쳐 진행되었습니다.

1단계: WebAssembly 기본 구현 (Gora Book 포팅)

  • Rust로 작성된 WebAssembly 학습 서적인 “Gora Book”의 개념을 순수 Ruby로 포팅하며 시작되었습니다.
  • WebAssembly 바이너리 파싱(프리앰블, 헤더, 섹션) 및 VM 실행 루프(명령어 패치-실행 반복)를 구현했습니다.
  • 외부 세계와의 상호작용을 위해 최소한의 fd_write 함수를 구현하여 “Hello World” 출력을 성공시켰습니다.

2단계: WebAssembly 코어 스펙 기본 명령어 구현

  • Ruby 실행에 대한 열망을 바탕으로, WebAssembly 코어 스펙의 기본 명령어 192개를 구현하는 데 집중했습니다.
  • i32, i64, f32, f64와 같은 4가지 숫자 타입에 대한 덧셈, 뺄셈 등 공통 명령어는 제너레이터를 활용하여 효율적으로 생성했습니다.

3단계: 그레이스케일 프로그램 실행 및 디버깅

  • Rust로 작성된 그레이스케일 변환 프로그램을 실행하려 했으나 초기에는 실패했습니다.
  • 메모리 할당 문제 해결 후 Rust 패닉이 unreachable로 번역되는 점을 활용하여 디버깅을 진행했습니다.
  • wasm-test-suite를 통해 i32 관련 테스트 케이스(특히 비트 시프트 연산)의 오류를 발견하고 수정하여 그레이스케일 프로그램 실행에 성공했습니다.

4단계: Ruby 실행을 위한 WASI (WebAssembly System Interface) 지원

  • Ruby 실행을 위해서는 WebAssembly 명령어 외에 WASI 지원이 필수적임을 인지했습니다.
  • 초기에는 ruby -v 명령어가 작동하도록 37개의 WASI 함수를 하나씩 구현했습니다(예: clock_time_getTime.now로 래핑).
  • 인수 처리, 현재 시간, 난수 생성, 환경 변수, 파일 디스크립터 작업 등 기본적인 시스템 상호작용 기능들을 구현하여 Ruby의 내장 코어 라이브러리(예: Integer#times)가 작동하게 했습니다.

5단계: 파일 시스템 (Pre-opens) 구현

  • require 기능의 정상 작동을 위해 파일 시스템 인식이 필요했으며, 이를 위해 WASI의 “pre-opens” 메커니즘을 구현했습니다.
  • 보안상의 이유로 WebAssembly 런타임은 기본적으로 호스트 파일 시스템에 접근할 수 없으므로, VM 시작 시 미리 등록된 파일 디스크립터(3, 4, 5)를 통해 접근 가능한 경로를 설정하는 방식입니다.
  • wasi_populate_preopens 함수를 통해 게스트 경로와 호스트 경로 매핑을 등록하는 관련 함수들을 구현한 결과, Ruby 명령어가 로딩 경고 없이 정상적으로 실행되었습니다.

성능 최적화

  • 블록 점프 최적화: WebAssembly 점프 명령어의 동적 타겟 계산 방식을 개선, 명령어 파싱 후 끝 위치를 미리 계산하고 캐싱하여 그레이스케일 벤치마크에서 실행 시간을 43% 단축했습니다.
  • 인스턴스 생성 감소: VM#set_id, RbClass#new_instance 등에서 발생하는 과도한 객체 인스턴스 생성(그레이스케일에서 1,880만 개의 정수 객체 생성)을 줄이기 위해 자주 사용되는 정수 인스턴스를 메모이제이션하여 약 1초의 속도 향상을 이루었습니다. 궁극적으로는 즉시 값(immediate values) 사용이 목표입니다.
  • YJIT 활용: Ruby 3.1과 함께 YJIT를 사용했을 때 52%, Ruby 3.4에서는 57%의 실행 시간 단축 효과를 보여, 성능 향상에 YJIT가 매우 효과적임을 입증했습니다.

결론

Roite 프로젝트는 순수 Ruby로 WebAssembly 런타임을 구현하는 도전적인 여정을 성공적으로 보여주었습니다. Wasm 코어 스펙과 WASI의 주요 기능을 구현하여 Ruby 자체를 Roite 위에서 실행하는 쾌거를 이루었으며, 성능 최적화를 통해 실용적인 가능성을 입증했습니다. 앞으로 WebAssembly 컴포넌트 모델 지원을 통해 Roite의 언어 불가지론적 특성을 더욱 강화하고, 지속적인 기여를 통해 프로젝트를 발전시켜 나갈 계획입니다. 순수 Ruby 기반 Wasm 런타임은 여전히 모험적인 영역이지만, Ruby 생태계에 새로운 가능성을 열어줄 것으로 기대됩니다.

댓글 0

댓글 작성

0/1000
정중하고 건설적인 댓글을 작성해 주세요.

아직 댓글이 없습니다

첫 번째 댓글을 작성해보세요!