0. はじめに
この記事は Houdini Apprentice Advent Calendar 2024 の20日目の記事です。
0-1. 動作環境
OS: RedHat Enterprise Linux 9.4
Houdini: 20.5.410 Py3.11 (最新の Production Build) Education Edition
Rust: rustc 1.82.0
0-2. 免責
この記事をもとに被った不利益について私は一切責任を負いません.
あと RHEL 以外での検証もしてないです。
1. モチベーション
TOPs って Python 書けるじゃないですか。Rust も書けたら面白いな〜って思っただけです。
2. 実装
2-1. ゴール
今回作るものです。
パラメータに Rust のコードを入力して cook するとワークアイテムの数だけ処理が走り、 stdout
アトリビュートに標準出力を返します。
2-2. 戦略
TOPsでは Python が使えますから、
- パラメータの文字列から
.rs
ファイル(Rust の拡張子です。Redshift じゃないよ)を作る。 -
.rs
ファイルをビルドする。 - できたバイナリを実行する。
この3つが Python からできれば勝ちということです。
ただし、実行は Out of Process
cooking で行うので、パラメータの評価などHoudiniにアクセスすることができません。
Out of Process
cooking では json ファイルを介してプロセス間通信をおこない、ワークアイテムのアトリビュートなどのデータにはアクセスできるので、パラメータの文字列をアトリビュートに移してやり取りすることにします。
1. に関しては $PDG_TEMP
に適当なディレクトリを切ってアトリビュートの文字列から .rs
ファイルを作ります。
2, 3. は Python の Subprocess を使って rustc とバイナリを叩きます。
2-3. パラメータの文字列をアトリビュートに移す
以下が Python スクリプトです。
読むとわかりますが、1つ上の階層の subnet のパラメータを見てます。
import hou
# annotation のため。別にいらない
from hou import Node
from pdg import AttributeInt
#この Python Script ノードを持つ subnet を表現する Node オブジェクト
HDA_node: Node = hou.pwd().parent()
#snippet パラメータを評価して文字列として受け取る
#subnet に snippet という名前の String タイプのパラメータを作る必要があるよ
source_code: str = HDA_node.evalParm("./snippet")
#ワークアイテムにアトリビュートを追加してアトリビュートオブジェクトを受け取る
snippet_attrib: AttributeInt = work_item.addAttrib("snippet", pdg.attribType.String)
#アトリビュートの値をプログラムの文字列にする
snippet_attrib.setValue(source_code, index = 0)
コメントにも書いてますが subnet に snippet
という名前の String 型のパラメータが必要です。
Multi-line String
にチェックいれて Lines to Show
を変えると縦に長くできます。
cook するとプログラムの全文が snippet
アトリビュートに書き込まれています。
2-4. ビルドして実行する
まずは $PDG_TEMP
ディレクトリに Rust
というディレクトリを作成し .rs
ファイルを作成します。
#PDG_TEMP環境変数を取得する
PDG_temp_dir: str = os.getenv('PDG_TEMP')
#ソースファイルを格納するディレクトリ(つまり $PDG_TEMP/Rust/src)
rust_src_dir: str = os.path.join(PDG_temp_dir, 'Rust', 'src')
#ソースファイル自体のパス(つまり $PDG_TEMP/Rust/src/main.rs)
rs_path = os.path.join(rust_src_dir, 'main.rs')
#バイナリのパス(つまり $PDG_TEMP/Rust/src/main)
binary_path = os.path.join(rust_src_dir, 'main')
#ディレクトリを作る
os.makedirs(rust_src_dir, exist_ok=True)
#ワークアイテムを取得
#out of process cooking では pdgjson.WorkItem.fromJobEnvironment() をコールすることでWorkItem オブジェクトを取得できる
work_item: WorkItem = WorkItem.fromJobEnvironment()
#ワークアイテムのアトリビュートからソースコードを取得
snippet: str = work_item.attribValue('snippet')
#ソースファイルを作る
with open(rs_path, 'w') as file:
file.write(snippet)
次にビルドします。
try:
#subprocessを使ってrustcを叩く
#-oでバイナリの出力先を指定できる
subprocess.run(['rustc', rs_path, '-o', binary_path], check = True)
except subprocess.CalledProcessError as e:
print(f"Error during rustc compilation: {e}")
raise
最後に実行します。
try:
#capture_output=True でプロセスの標準出力が取れる
#text=True でバイナリではなく文字列の出力が取れる
#.stdout で文字列の標準出力を返す
stdout: str = subprocess.run([binary_path], check = True, capture_output = True, text = True).stdout
#ワークアイテムにstdoutアトリビュートを作成し、アトリビュートオブジェクトを返す
output_attrib: AttributeString = work_item.addAttrib("stdout", pdg.attribType.String)
#アトリビュートの値を標準出力の文字列にする
output_attrib.setValue(stdout)
except subprocess.CalledProcessError as e:
print(f"Error during binary execution: {e}")
raise
以下がスクリプト全体です。
この Python Script は Out-Of-Process です。
import os
import subprocess
import hou
#annotation のためなので別にいらない。
from hou import Node
from pdgjson import WorkItem
from pdgjson import AttributeString
PDG_temp_dir: str = os.getenv('PDG_TEMP')
rust_src_dir: str = os.path.join(PDG_temp_dir, 'Rust', 'src')
rs_path = os.path.join(rust_src_dir, 'main.rs')
binary_path = os.path.join(rust_src_dir, 'main')
os.makedirs(rust_src_dir, exist_ok=True)
work_item: WorkItem = WorkItem.fromJobEnvironment()
snippet: str = work_item.attribValue('snippet')
with open(rs_path, 'w') as file:
file.write(snippet)
try:
subprocess.run(['rustc', rs_path, '-o', binary_path], check=True)
except subprocess.CalledProcessError as e:
print(f"Error during rustc compilation: {e}")
raise
try:
stdout: str = subprocess.run([binary_path], check=True, capture_output=True, text = True).stdout
output_attrib: AttributeString = work_item.addAttrib("stdout", pdg.attribType.String)
output_attrib.setValue(stdout)
except subprocess.CalledProcessError as e:
print(f"Error during binary execution: {e}")
raise
cook するときちんと stdout
アトリビュートに出力が格納されていると思います。
最後に attribute delete
で不要な snippet
アトリビュートを消しておきます。
3. おわりに
誰得なんだという記事ですが、最近遊んでたものを紹介してみました。
pdgjson を操作するクレートを作れば Rust からワークアイテムをいじれるのかも...?
あとシンタックスハイライトも入れられたらいいですね。
最後まで読んでいただきありがとうございました!