TOPsでRustを動かす

# 0. はじめに
　この記事は [Houdini Apprentice Advent Calendar 2024](https://qiita.com/advent-calendar/2024/happrentice) の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. ゴール
今回作るものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/36e699e3-b999-9e67-9a6b-3cea6063193c.png)
　パラメータに Rust のコードを入力して cook するとワークアイテムの数だけ処理が走り、 ```stdout``` アトリビュートに標準出力を返します。
### 2-2. 戦略
　TOPsでは Python が使えますから、
 1. パラメータの文字列から ```.rs``` ファイル(Rust の拡張子です。Redshift じゃないよ)を作る。
 2. ```.rs``` ファイルをビルドする。
 3. できたバイナリを実行する。

この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 のパラメータを見てます。
```python
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``` を変えると縦に長くできます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/b34339df-3121-d5b3-8a63-34ae8e9aaa73.png)

　cook するとプログラムの全文が ```snippet``` アトリビュートに書き込まれています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/330f599c-8595-cf96-5b09-cb4f007b0625.png)

### 2-4. ビルドして実行する
　まずは ```$PDG_TEMP``` ディレクトリに ```Rust``` というディレクトリを作成し ```.rs``` ファイルを作成します。
```python
#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)
```
　実行するとこういう感じのディレクトリ構造になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/efc51d7c-fd57-71eb-378d-cd473b701337.png)

　次にビルドします。
```python
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
```

　最後に実行します。
```python
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
```

　以下がスクリプト全体です。

:::note warn
この Python Script は Out-Of-Process です。
:::
```python
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``` アトリビュートに出力が格納されていると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/e94a27c2-fbc7-609a-4889-4cc0eb45b36e.png)

　最後に ```attribute delete``` で不要な ```snippet``` アトリビュートを消しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/565633/6a7b4a43-d494-2e45-5840-4b54367054e1.png)

# 3. おわりに
　誰得なんだという記事ですが、最近遊んでたものを紹介してみました。
　pdgjson を操作するクレートを作れば Rust からワークアイテムをいじれるのかも...?
　あとシンタックスハイライトも入れられたらいいですね。

　最後まで読んでいただきありがとうございました！

# 4. 参考
[Job API](https://www.sidefx.com/ja/docs/houdini/tops/jobapi.html)
[pdg package](https://www.sidefx.com/ja/docs/houdini/tops/pdg/)
[Out-Of-Processの基本を理解する(1)](https://fereria.github.io/reincarnation_tech/houdini/pdg/oop/01)