pywebview on Mojo🔥
pywebview
をMojo 🔥でコンパイルする
已前、pywebview
が使えることを確認した。
それはつまり、pywebview
を物する準備が整ったことに外ならない。
インストール
pywebview
を扱うには、種々のインストールが必要となる。apt
すら且つ選択の余地多く煩わしいことを、況や
プロジェクト作成
pywebview_mojo
の名でプロジェクトを作った。
pixi init pywebview_mojo -c https://conda.modular.com/max-nightly/ -c conda-forge
pixi.toml
はこのようになった。[tasks]
の項は本旨と関係ないため、特段気にせずともよい。
[workspace]
channels = ["https://conda.modular.com/max-nightly", "conda-forge"]
name = "pywebview_mojo"
platforms = ["linux-64"]
version = "0.1.0"
[tasks]
pymain = "python src/python/main.py"
mjtest = "mojo test/test.mojo"
mjmain = "mojo src/mojo/main.mojo"
mkdir = "mkdir -p build"
build = { cmd = "mojo build src/mojo/main.mojo -o build/main", depends-on = ["mkdir"], inputs = ["src/mojo/main.mojo"], outputs = ["build/main"]}
exe = "build/main"
run = [{ task = "build" }, { task = "exe" }]
clean = "rm -rf build"
[dependencies]
# Mojo
modular = ">=25.6.0.dev2025090605,<26"
# Python
python = ">=3.13.7,<3.14"
# pkgs for pywebview
pyqt = ">=5.15.11,<6"
pyqtwebengine = ">=5.15.11,<6"
pyqtwebkit = ">=5.15.11,<6"
pygobject = ">=3.50.0,<4"
gtk3 = ">=3.24.43,<4"
[pypi-dependencies]
# pywebview from PyPI
pywebview = { version = ">=6.0, <7", extras = ["qt"] }
Python で動作確認
先ずは
ソースコード
import webview
from MyApi import Api
api = Api()
webview.create_window(
title = 'App',
url = 'static/index.html',
js_api = api
)
webview.start()
import webview
class Api:
def __init__(self):
self._logs = []
def log(self, value):
self._logs.append(value)
print(f"log: {value}")
return "Logged: " + value
def get_logs(self):
print("get_logs: called")
return self._logs
def exit(self):
webview.active_window().destroy()
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>PyWebView チュートリアル</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<h1>PyWebView チュートリアル</h1>
<button onclick="sendLog()" class="main-btn">ログを送信</button>
<button onclick="showLogs()" class="main-btn">履歴を表示</button>
<button onclick="exitApp()" class="exit-btn">終了</button>
<div id="logs"></div>
<script src="scripts.js"></script>
</body>
</html>
body {
font-family: sans-serif;
margin: 2em;
}
#logs {
margin-top: 1em;
background: #f4f4f4;
padding: 1em;
border-radius: 8px;
}
button {
padding: 0.7em 1.5em;
margin-right: 0.5em;
border: none;
border-radius: 6px;
font-size: 1em;
cursor: pointer;
transition: background 0.2s;
}
.main-btn {
background: #4caf50;
color: white;
}
.main-btn:hover {
background: #388e3c;
}
.exit-btn {
background: #f44336;
color: white;
}
.exit-btn:hover {
background: #c62828;
}
function showResponseTime(start, label) {
const elapsed = performance.now() - start;
document.getElementById('logs').innerHTML +=
`<div><em>${label} 応答時間: ${elapsed.toFixed(3)} ms</em></div>`;
}
function sendLog() {
const start = performance.now();
pywebview.api.log("Let's play PyWebView!").then(response => {
showResponseTime(start, "ログ送信");
alert(response);
});
}
function showLogs() {
const start = performance.now();
pywebview.api.get_logs().then(logs => {
document.getElementById('logs').innerHTML =
"<strong>ログ履歴:</strong><br>" + logs.map(l => `<div>${l}</div>`).join('');
showResponseTime(start, "履歴表示");
});
}
function exitApp() {
pywebview.api.exit();
}
実行の様子
キャプチャーには映っていないが、「ログを送信」ボタンを押した後、毎回次のようなウィンドウが出てきている。
Mojo 🔥による実装
本題:
0. API について
pywebview
を扱うに当たり、class
を定めていたことが障る。
︙
api = Api()
webview.create_window(
title = 'App',
url = 'static/index.html',
js_api = api
)
︙
︙
class Api:
def __init__(self):
self._logs = []
def log(self, value):
︙
前提知識として、class
はなく、struct
が存在する。とは言え、これ自体は問題の本質ではない。
PythonObject
|
||
---|---|---|
List |
python.Python.list |
|
Dict |
python.Python.dict |
|
Tuple |
python.Python.tuple |
なお、PythonObject
として扱われることから、PythonObject
となる。
Python にはPythonObject
を
webview.create_window()
に与えるclass
でなければならない』。
なお現在、Python
構造体にはclass
などという変数も関数もないようである。
Mojo 🔥でPython を記述する
一、言語によっては、sys._assembly.inlined_assembly
を使うことでアセンブリーを記述・実行することができる。
これと似て、
つまり次のように、class
を記述することができるのである。
# PythonによるApiクラスの定義
var class_definition: String = """
class Api:
_hello: str = 'Hello Mojo'
def hello(self) -> str:
return self._hello
"""
# ApiClassDefinitionという仮想上のPythonモジュールを作る
# ApiClassDefinition.pyというファイルが存在するような状態(実際には存在しない)
var ApiClassDefinition: PythonObject = Python.evaluate(
expr = class_definition,
file = True,
name = 'ApiClassDefinition'
)
# ApiClassDefinitionモジュールからApiクラスを呼び出す
api = ApiClassDefinition.Api()
# 'Hello Mojo'と表示される
print(api.hello())
動くプログラムの例
動作はこの次に示すものと同じ。但し、こちらの方が起動に時間が掛かった。
from python import Python, PythonObject
fn main():
var class_definition: String = """
class Api:
_hello: str = 'Hello Mojo'
def hello(self) -> str:
return self._hello
"""
try:
var webview: PythonObject = Python.import_module('webview')
var ApiClassDefinition: PythonObject = Python.evaluate(
expr = class_definition,
file = True,
name = 'ApiClassDefinition'
)
api = ApiClassDefinition.Api()
# print(api.hello())
webview.create_window(
title = 'App Mojo',
html = '''
<p>pywebview with mojo</p>
<p><button onclick="pywebview.api.hello().then(response => {document.getElementById('hello').innerHTML = response;})">button</button></p>
<p id="hello"></p>
''',
js_api = api,
width = 200,
height = 200
)
webview.start()
except e:
print('exception occurred: ', e)
これでも構わないが、端からファイルとして分けた方が整頓の観点では宜しいと感ぜられよう。実際、MyApi.py
というファイルを作っている。
.py
ファイルを読み込む
一、.py
ファイルを読み込むにあたり、一度確認のためtest
というフォルダーの中で行う。
ソースコード
from python import Python, PythonObject
fn main():
try:
var webview: PythonObject = Python.import_module('webview')
var Api: PythonObject = Python.import_module('Api')
var api: PythonObject = Api.Api()
webview.create_window(
title = 'App Mojo',
html = '''
<p>pywebview with mojo</p>
<p><button onclick="pywebview.api.hello().then(response => {document.getElementById('hello').innerHTML = response;})">button</button></p>
<p id="hello"></p>
''',
js_api = api,
width = 200,
height = 200
)
webview.start()
except e:
print('exception occurred: ', e)
class Api:
_hello: str = 'Hello Mojo'
def hello(self) -> str:
return self._hello
実行の仕方
︙
[tasks]
︙
mjtest = "mojo test/test.mojo"
︙
pixi.toml
中にあるこの箇所により、test.mojo
を実行する時は次のように簡略化されている。
pixi run mjtest
このようなタスクを定めない場合、次のように長いコマンドとなる。
pixi run mojo test/test.mojo
画像のような画面が表示される。ボタンを押すと文字が出るという簡素なものである。
これで、.py
ファイルに定義されたclass
を
1. Mojo 🔥の記述
先の検証で概ね実装方法は判明しているが、「パスの指定」のみ注意を要した。
ここで、src
フォルダーの中にmojo
フォルダーを新たに作る。
from python import Python, PythonObject
fn main():
try:
# パスを追加しなければ MyApi.py が探されない
Python.add_to_path("src/python")
var webview: PythonObject = Python.import_module('webview')
var MyApi: PythonObject = Python.import_module('MyApi')
var api: PythonObject = MyApi.Api()
webview.create_window(
title = 'App Mojo',
# pywebview_mojo(プロジェクトルート)から示さなければ探されない
url = 'pywebview_mojo/src/python/static/index.html',
js_api = api
)
webview.start()
except e:
print('exception occurred: ', e)
二箇所でファイルパスを示しているが、その書き方がやや異なることが分かるだろう。私の試した限りでは、この通りでなければエラーになった。
src
から始まるパス
# パスを追加しなければ MyApi.py が探されない
Python.add_to_path("src/python")
今、src/mojo/main.mojo
とsrc/python/MyApi.py
とは異なるフォルダーに存在することから、その位置を示さねばならない。これに用いるのがpython.Python.add_to_path
である。
この場合には、src
から始めることで正しく探すことができるようだった。
プロジェクトルートから始まるパス
webview.create_window(
title = 'App Mojo',
# pywebview_mojo(プロジェクトルート)から示さなければ探されない
url = 'pywebview_mojo/src/python/static/index.html',
js_api = api
)
これはindex.html
を基に画面を作る箇所であるため、やはりその位置を正確に示さなければならない。しかし先の箇所とは異なり、プロジェクトルートのフォルダー名であるpywebview_mojo
から示さなければ、正しく探されなかった。
実行
︙
[tasks]
︙
mjmain = "mojo src/mojo/main.mojo"
︙
pixi.toml
中にあるこの箇所により、main.mojo
を実行する時は次のように簡略化されている。
pixi run mjmain
このようなタスクを定めない場合、次のように長いコマンドとなる。
pixi run mojo src/mojo/main.mojo
2. 実行ファイルへのコンパイル
︙
[tasks]
︙
mkdir = "mkdir -p build"
build = { cmd = "mojo build src/mojo/main.mojo -o build/main", depends-on = ["mkdir"], inputs = ["src/mojo/main.mojo"], outputs = ["build/main"]}
exe = "build/main"
run = [{ task = "build" }, { task = "exe" }]
︙
pixi.toml
中にあるこの箇所により、build
フォルダーの作成とコンパイル、実行までを次のコマンドで演じさせることができる。
# pixi run (タスク名)
pixi run run
実行時の注意
此度に限っては、このような実行方法でなければエラーになる。つまり、コンパイルで生成された実行ファイルを直接実行することができないのである。その原因は二つ。
-
pywebview
を探せなくなる -
やQt が探せなくなるGTK
pywebview
は.pixi
という隠しフォルダーに全て収められている。ここからwebview
を探してパスを指定すれば、Python.app_to_path
を使ってエラーを解決することができる。
apt
で
実行の様子
1ms
台の応答時間が多く見られた気がするが、殆ど誤差だろう。コンパイルしたことで歴然の差が生まれるといったことはなさそうである。
跋
今回、pywebview
すら実行ファイルで探せない、即ち外部ファイルに依存したままで内包できないことは想定していなかった。コンパイルできると雖も、その配布には少し気を遣うらしい。
Discussion