RustPython은 기존에 C언어로 작성되어 있는 Python 인터프리터 런타임을 Rust 언어로 재작성한 프로젝트이다. 웹 어셈블리와 궁합이 좋은 Rust 언어의 특징상, RustPython도 웹 어셈블리로 컴파일한 후 브라우저에서 별도의 서버 없이 파이썬 인터프리터를 구동하는 것이 가능하다.
이렇게 하면 브라우저에서 파이썬 온라인 에디터와 실행기를 제공하거나, 파이썬으로 웹 앱의 플러그인을 작성하는 등의 기능들을 만들 수 있다.
그런데 RustPython를 웹어셈블리로 컴파일해 사용하는 방법에 대한 문서는 굉장히 빈약한 편이다. 가이드 문서를 보면, 데모 프로젝트를 실행하는 법에 대해서만 적혀 있고, 어떻게 컴파일해 사용하는지에 대한 정보나 사용할 수 있는 함수들의 정보가 포함되어 있지 않다. 그래서 직접 RustPython 코드를 뜯어보며 알아낸 내용을 바탕으로, 사용 방법에 대한 가이드를 작성하고자 한다.
브라우저에서 웹 어셈블리로 동작하는 RustPython을 React 환경에서 빌드하고 사용하는 방법을 알아보자.
RustPython을 웹 어셈블리로 빌드하려면 Rust와 wasm-pack이 설치되어 있어야 한다.
먼저 RustPython 리포지토리를 로컬에 clone 받자.
$ git clone https://github.com/RustPython/RustPython.git
$ cd RustPython
RustPython 리포지토리의 wasm
디렉터리에 웹어셈블리를 위한 코드가 모여 있다. 여기서 wasm/lib
디렉터리가 RustPython을 웹어셈블리로 컴파일하기 위한 진입점이다.
$ cd ./wasm/lib
$ wasm-pack build --target web
그러면 다음과 같이 pkg
디렉터리에 자바스크립트 환경에서 바로 사용할 수 있는 모듈이 생성된다.
Vite로 리액트 프로젝트를 생성하자. 프로젝트 생성 메뉴가 나오면 React + TypeScript 를 선택하면 된다.
$ vite create react-rustpython
src
디렉터리에 python
디렉터리를 만들고, 위에서 생성한 모듈을 rustpython 디렉터리를 만들어 다음과 같이 복사하자.
Vite 환경에서 RustPython 웹 어셈블리를 로드하는 src/python/index.ts
파일을 다음과 같이 작성하자.
import init, { vmStore } from "./rustpython/rustpython_wasm";
import wasmUrl from "./rustpython/rustpython_wasm_bg.wasm?url";
let loaded = false;
export async function createPythonVm() {
if (!loaded) {
loaded = true;
await init(wasmUrl);
}
const vm = vmStore.init("rust_python_example", false);
vm.setStdout(console.log);
return vm;
}
createPythonVm
함수를 호출하면, RustPython 웹어셈블리가 로드되고 Python VM 객체가 반환된다.
vmStore.init 메서드의 첫번째 인자는 Python VM의 고유 ID이고, 두번째 인자는 inject_browser_module
이라는 이름을 가지고 있다. inject_browser_module
인자가 true라면 생성된 VM의 Python 코드에서 브라우저의 DOM 객체나 fetch, alert등의 함수를 사용할 수 있는 browser
모듈을 사용할 수 있다. 그러나 이 기능은 예상치 못한 보안 위협에 노출될 수 있기 때문에, 꼭 필요하지 않은 한 false
로 설정해 사용하지 않는 것이 좋다. 이 기능을 사용하는 예제는 RustPython의 다음 코드 에서 볼 수 있다.
Vite 공식 가이드에서는 웹 어셈블리를 init하기 위해 다른 방법을 제시하고 있지만, RustPyhton 웹 어셈블리 모듈에서는 잘 동작하지 않는다. 따라서 위의 코드에서는 이 이슈 글을 참고해 웹 어셈블리를 init 하였다.
이제 유저가 한 줄의 Python expression을 입력하고 버튼을 누르면 해당 expression을 실행하는 Eval 컴포넌트를 만들어보자.
import { useState } from "react";
import { createPythonVm } from "./python";
function Eval() {
const [input, setInput] = useState("");
const [output, setOutput] = useState("");
async function run() {
const python = await createPythonVm();
python.addToScope("hello", "world");
python.addToScope("alert", (message: string) => alert(message));
const result = python.eval(input);
setOutput(`${result}`);
}
return (
<div>
<h1>Eval</h1>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={run}>Run</button>
<div>
<h2>Output</h2>
<pre>{output}</pre>
</div>
</div>
);
}
export default Eval;
.addToScope
메서드는 실행할 Python 환경에 임의의 변수를 추가할 수 있다. 이렇게 추가한 변수는 파이썬에서 전역 변수로 바로 사용할 수 있다.
.eval
메서드는 인자로 들어온 Python expression 을 실행시키고 그 결과를 반환한다. 한 줄의 파이썬 expression만 실행시킬 수 있으므로, 다른 모듈을 import해 사용하거나 복잡한 조건문이나 반복문을 사용할 수 없다.
이번에는 .exec
메서드를 사용해서 여러 줄의 파이썬 코드를 실행시키고, print
한 결과를 보여주는 Exec 컴포넌트를 만들어보자.
import { FC, useState } from "react";
import { createPythonVm } from "./python";
const Exec: FC = () => {
const [output, setOutput] = useState("");
async function run() {
const python = await createPythonVm();
python.setStdout((message: unknown) =>
setOutput((output) => output + `${message}`)
);
python.addToScope("__name__", "__main__");
python.injectJSModule("module_js", {
add(a: unknown, b: unknown) {
if (typeof a !== "number" || typeof b !== "number") {
throw new Error("a and b should be numbers.");
}
return a + b;
},
});
python.injectModule("module_py", modulepy, {});
python.exec(sourcepy, "source.py");
}
return (
<div>
<h1>Exec</h1>
<button onClick={run}>Run</button>
<h2>Output</h2>
<pre>{output}</pre>
</div>
);
};
export default Exec;
const modulepy = `
def mul(a, b):
return a * b
`;
const sourcepy = `
import module_js
import module_py
if __name__ == '__main__':
print("Add: " + str(module_js.add(5, 8)))
print("Mul: " + str(module_py.mul(5, 8)))
`;
.setOutput
메서드는 파이썬에서 표준 출력으로 문자열을 내보낼 때 호출되는 콜백을 지정할 수 있다. 여기서는 출력 문자열을 받아서 output state 값의 끝에 추가하는 콜백을 넣었다. 따라서 이 경우 print
함수의 결과가 React의 output 상태의 끝에 추가된다.
.injectJSModule
과 .injectModule
메서드는 둘 다 Python 모듈을 실행 환경에 집어넣어서, 파이썬 코드에서 import
문으로 가져올 수 있도록 해주는데, 둘의 약간의 차이가 있다.
.injectModule
메서드는 모듈을 Python 코드의 문자열로 넣을 수 있는데, 첫번째 인자는 Python 코드의 import
문에 사용할 모듈 이름, 두번째 인자는 모듈의 Python 코드이고, 세번째 인자는 해당 모듈 코드에서 사용할 수 있는 전역 변수들을 직접 넣어줄 수 있다.
.injectJSModule
메서드는 자바스크립트 객체를 넣으면, 이를 Python 모듈로 변환해 실행 환경에 집어넣는다. 첫번째 인자는 import
문에 사용할 모듈 이름, 두번째 인자는 모듈로 들어갈 자바스크립트 객체이다.
.exec
메서드는 첫번째 인자로 들어온 Python 코드를 실행시킨다. 이 때 두번째 인자로 들어온 문자열을 현재 파일 이름으로 인식한다.
앞선 Exec 컴포넌트에서는 Python 코드의 실행 결과를 print
함수를 통해 표준 출력으로 받았다. 그러나 표준 출력은 결과를 문자열로 전달하기 때문에 데이터를 자바스크립트와 주고 받기가 까다롭다. 여기서 .injectJSModule
메서드를 이용해 데이터를 주고받는 함수를 넣어주면, 복잡한 데이터도 문제없이 전달할 수 있다. 이를 구현한 Bridge 컴포넌트를 만들어보자.
import { FC, useState } from "react";
import { createPythonVm } from "./python";
const Bridge: FC = () => {
const [input, setInput] = useState("");
const [output, setOutput] = useState<number | null>(null);
async function run() {
const python = await createPythonVm();
python.injectJSModule("bridge", {
getInput() {
return parseInt(input, 10);
},
setResult(value: unknown) {
if (typeof value !== "number") {
throw new Error("value type is not number. value: " + value);
}
setOutput(value);
},
});
python.injectModule("fibo", fibopy);
python.exec(mainpy, "main.py");
}
return (
<div>
<h1>Fibo</h1>
<input
type="text"
value={input}
onChange={(e) => setInput(e.target.value)}
/>
<button onClick={run}>Run</button>
<p>결과: {output}</p>
</div>
);
};
export default Bridge;
const mainpy = `
import bridge
from fibo import fibo
user_input = bridge.getInput()
result = fibo(user_input)
bridge.setResult(result)
`;
const fibopy = `
cache = {}
def fibo(n):
if n <= 1:
return n
if cache.get(n) != None:
return cache.get(n)
r = fibo(n-2) + fibo(n-1)
cache[n] = r
return r
`;
Bridge 컴포넌트에서는 유저가 정수를 입력하면, 해당 수 번째의 피보나치 수열을 출력한다. Python 코드를 보면 .injectJSModule
메서드로 넣어준 bridge 모듈을 import해 React 상태에 접근하는 것을 확인할 수 있다.
지금은 입력과 출력에 대한 데이터만 전달하지만, 조금 더 고도화 하면 Python에서 발생한 에러를 안전하게 자바스크립트에 전달하는 동작 등도 구현할 수 있다.
개인 React 프로젝트에서 RustPython을 사용하고 싶어 자료를 검색했을 때, 공식 문서도 빈약하고 영문 구글에서도 관련된 정보가 전혀 나오지 않아 굉장히 당황스러웠다. 이 글을 통해 누군가 브라우저에서 RustPython을 사용할 때 삽질을 덜 하게 된다면 좋을 것 같다.
https://github.com/Tekiter/react-rustpython-example 에서 위의 예제에 대한 전체 코드를 볼 수 있다.