5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RustPython (-> WASM) + bun (+ preact) -> Happy💫

5
Last updated at Posted at 2026-02-06

ブラウザだけでPythonが動く環境を作った

Your Python のスクリーンショット

インストール不要、ブラウザだけでPythonが書けて実行できる環境を作りました。

なぜ作ったか

Pythonを学び始めるとき、環境構築でつまずくことが多いです。「とりあえずPythonを触ってみたい」という人が、ブラウザを開くだけで始められる場所があればいいなと思って作りました。

技術スタック

カテゴリ 技術
Pythonランタイム RustPython (WASM)
UIライブラリ Preact
コードエディタ CodeMirror 6
スタイリング Tailwind CSS v4
ビルド / ランタイム Bun
リンター Biome
デプロイ GitHub Pages

RustPythonとは

RustPythonはRustで書かれたPythonインタプリタです。WebAssemblyにコンパイルできるため、ブラウザ上でPythonコードを実行できます。

似たプロジェクトにPyodide(CPythonベース)がありますが、RustPythonはRust製なのでWASMとの親和性が高いです。

WASMの初期化

import init, { pyExec } from "../wasm/rustpython_wasm.js";
import wasmUrl from "../wasm/rustpython_wasm_bg.wasm";

let initialized = false;
let initPromise: Promise<void> | null = null;

export async function initPythonRuntime(): Promise<void> {
	if (initialized) return;

	if (initPromise) return initPromise;

	initPromise = (async () => {
		await init({ module_or_path: wasmUrl });
		initialized = true;
	})();

	return initPromise;
}

WASMバイナリは約50MBあるので、初期化は非同期で行います。initPromiseを保持することで、複数回呼ばれても初期化が一度だけ実行されるようにしています。

Pythonコードの実行

export function executePython(code: string, inputs: string[]): ExecutionResult {
	let stdout = "";
	let error: string | null = null;

	const fullCode = buildPrelude(inputs) + code;

	try {
		pyExec(fullCode, {
			stdout: (output: string) => {
				stdout += output;
			},
		});
	} catch (err) {
		if (err instanceof WebAssembly.RuntimeError) {
			const globalError = (globalThis as { __RUSTPYTHON_ERROR?: string })
				.__RUSTPYTHON_ERROR;
			error = globalError ?? String(err);
		} else {
			error = String(err);
		}
	}

	return { stdout, error };
}

pyExecにコードとstdoutコールバックを渡すだけで実行できます。エラーが発生した場合、RustPythonはWebAssembly.RuntimeErrorをスローし、__RUSTPYTHON_ERRORグローバル変数にPythonのトレースバック情報を格納します。

input()の対応

ブラウザでPythonのinput()を動かすのは少し工夫が必要です。WASMは同期的に実行されるため、ユーザーの入力を途中で待つことができません。

そこで、事前に入力値をキューとして渡す仕組みにしました。

function buildPrelude(inputs: string[]): string {
	const inputsLiteral = JSON.stringify(inputs);
	return `\
import browser
import builtins

_input_queue = ${inputsLiteral}
_input_index = 0

def _custom_input(prompt=""):
    global _input_index
    if _input_index < len(_input_queue):
        result = _input_queue[_input_index]
        _input_index += 1
        return result
    return browser.prompt(prompt)

builtins.input = _custom_input
`;
}

ユーザーコードの前にこのプレリュードを挿入して、builtins.inputを差し替えています。キューが尽きた場合はbrowser.prompt()(ブラウザのプロンプトダイアログ)にフォールバックします。

UIにはInputパネルを用意していて、1行に1つの入力値を書きます。

プリレンダリング

WASMが50MBあるので初回読み込みに時間がかかります。ユーザーが白い画面を見続けなくて済むように、ビルド時にPreactコンポーネントをHTMLに埋め込んでいます。

import { h } from "preact";
import { render as renderToString } from "preact-render-to-string";
import { App } from "./src/App";

const appHtml = renderToString(h(App, null));
const preRendered = html.replace(
	'<div id="root"></div>',
	`<div id="root">${appHtml}</div>`,
);

ビルド時にrenderToStringでAppコンポーネントをHTML文字列に変換し、index.htmlに注入します。フロントエンドではrenderではなくhydrateを使うことで、プリレンダリングされたDOMをそのまま再利用します。

おまけ: Bunのビルドスクリプト

ViteやWebpackは使わず、BunのBun.build() APIだけでビルドしています。

const result = await Bun.build({
	entrypoints,
	outdir,
	plugins: [plugin],  // bun-plugin-tailwind
	minify: true,
	target: "browser",
	sourcemap: "linked",
	define: {
		"process.env.NODE_ENV": JSON.stringify("production"),
	},
});

HTML、TypeScript、Tailwind CSSのバンドル、minify、ソースマップ生成がこれだけで完結します。ビルドツールの設定ファイルが不要なのはかなり楽です。

まとめ

  • RustPython (WASM) でブラウザ上にPython実行環境を構築
  • input()はキュー + browser.prompt()フォールバックで対応
  • プリレンダリングで初回表示を高速化
  • Bunだけでビルドが完結


Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?