ADVENT CALENDAR 2019

初めての組み込みスクリプティング(C# / Iron系スクリプト言語 編)

By 檸檬茶(Lemon TEA)

前回の記事では,C#で作成したプログラムにLuaで記述されたスクリプトを組み込む方法を示しました. 他に面白いものはないものかとG●●gle先生に聞いてみたら,前回使ったKeraLuaよりヤベェやつを見つけました. その名も……

_人人人人人人人_
> Iron Python <
 ̄Y^Y^Y^Y^Y^Y ̄

どこかで聞いたことがあるという人もいるかもしれません. そう,IronPythonとは,.NETやMonoで動作するPythonのことです. しかしこいつは本当にヤベェやつでして,普通のPython(CPythonと呼ばれます)に加え,.NETのもつ豊富なライブラリをPython2.x系の文法でそのまま使えるというメリットを持ちます. 現在ではpython3.x系の文法でも書けるのだとか…… C#のライブラリの中に,このIronPythonを組み込むためのものが存在するので,今回はそれを弄くり回して行こうと思います.

なお私の実行環境は以下の通りです.

  • MacOS Catalina 10.15.1
  • Visual Studio for Mac 2019 8.3.10
  • mono 6.4.0.208
  • IronPython 3.0 (3.0.0.0) on .NET 4.0.30319.42000

今回の記事の作成にあたり,以下の文献を参考にしました.

事前準備

其の壱 : IronPythonライブラリの導入

例の如く Visual Studio プロジェクトを作成したら,NuGetからインストールしましょう.

ACAC-2019-12-25(1).png

しかし注意して欲しいのが,C#とIronPythonの連携プログラムを作成してそのまま実行すると,Microsoft.CSharp.RuntimeBinder.CsharpArgumentInfo.Createがないぞとコンパイラから怒られます. そのため,下に示すライブラリも同時に導入しておく必要があります.

ACAC-2019-12-25(2).png

其の弐 : using ディレクティブ

この記事では,C#にIronPythonを組み込む際のサンプルソースコードを示していきますが,以下の名前空間をusingしていることを前提としています.

using IronPython.Hosting;
using IronPython.Runtime;
using Microsoft.Scripting.Hosting;

実際に組み込んでみる

其の壱 : スクリプトを丸々実行する

C#で作成したプログラムにIronPythonを組み込む際,ScriptEngineScriptScopeScriptSourceの3つのクラスが登場します. 主な役割は下図の通りです.

クラス 主な役割
ScriptEngine スクリプトを実行するクラス
ScriptScope 変数やクラスの定義情報を格納するクラス
ScriptSource スクリプトのコード情報を格納するクラス

スクリプトを丸々実行するには,IronPythonの実行周りを統括するScriptEngineクラスのインスタンスを作成します.

ScriptEngine engine = Python.CreateEngine();

このengineは使いまわします.

次に,他の2つのクラスのインスタンスを,engineを介して生成します. スクリプトスコープを生成するにはCreateScopeメソッド,スクリプトソースを作成するにはCreateSourceメソッドを使用します. 但しCreateSourceメソッドについては,引数に実行するIronPythonファイルを指定します.

ScriptScope scope = engine.CreateScope();
ScriptSource source = engine.CreateScriptSourceFromFile("script.py");

スクリプトを実行するには,sourceのメソッドであるExecuteを実行します. 引数に定義情報,すなわちscopeを指定します.

source.Execute(scope);

このようにすることで,変数や関数などの定義をスクリプトに渡し,与えられた定義をスクリプトで実行することができます.

以上をまとめると,IronPythonスクリプトを実行するだけのプログラムは以下のようになります.

Console.WriteLine("[ 其の壱 : スクリプトを丸々実行する ]\n");

// スコープとソースを生成
ScriptScope scope1 = engine.CreateScope();
ScriptSource source1 = engine.CreateScriptSourceFromFile("script1.py");

// スクリプトの実行
source1.Execute(scope1);

Console.WriteLine("\n\n");

この部分が実行されたとき,例えばscript1.pyに以下のような記述がされているならば

print("Hello world from iron python!")

コンソールにはこのように出力されるはずです.

[ 其の壱 : スクリプトを丸々実行する ]

Hello world from iron python!

其の弐 : 変数の受け渡し

スクリプトの実行ができたら次は変数の受け渡しです. とここで,疑問に思う方がいるかもしれません. そう,C#にIronPythonを組み込むとき,スタックを利用しません. スクリプトスコープが実質スタックみたいなものなので,実行後の変数の情報は全てスクリプトスコープから取得できますし,スクリプトスコープに適当に変数の定義をぶち込んでおけば,あとはスクリプトエンジンがスクリプトをよしなに実行してくれます.

IronPythonで定義された変数をC#で取得する

スクリプト上の変数の情報を取得したい場合は,一度スクリプトを実行する必要があります. sourceExecuteメソッドを,scopeを引数にして実行してください.

source.Execute(scope);

スクリプトスコープから変数の情報を読み出すときはGetVariableメソッドを使います. 引数にはスクリプト上の変数の名前を文字列型で指定します.

scope.GetVariable("variable");

GetVariableメソッドの戻り値はdynamicなので,取得した値をキャストすることなく好き勝手にいじることができます. 但し,dynamicdynamicのままいじるのは実行時エラーの原因になりうるので,変数に格納されている値の型が分かっているならば,戻り値を目的の型にキャストして使用するとおりこうさんです.

以上をまとめると,IronPythonで定義された変数をC#で取得するプログラムは以下のようになります.

Console.WriteLine("[ 其の弐(壱) : IronPythonで定義された変数をC#で取得する ]\n");

// スコープとソースを生成
ScriptScope scope2_1 = engine.CreateScope();
ScriptSource source2_1 = engine.CreateScriptSourceFromFile("script2-1.py");

// スクリプトの実行(読み込み)
source2_1.Execute(scope2_1);

// グローバル変数の読み出し
Console.WriteLine("number = " + scope2_1.GetVariable("number"));
Console.WriteLine("string = " + scope2_1.GetVariable("string"));
Console.WriteLine("boolean = " + scope2_1.GetVariable("boolean"));

Console.WriteLine("\n\n");

このコードはGetVariableメソッドの戻り値をdynamicのまま使用しているのでおりこうさんじゃないですね(自己啓蒙). それはさておき,この部分が実行されたとき,例えばscript2-1.pyに以下のような記述がされているならば

number = 3.1415925
string = "This is a iron python script."
boolean = True

コンソールにはこのように出力されるはずです.

[ 其の弐(壱) : IronPythonで定義された変数をC#で取得する ]

number = 3.1415925
string = This is a iron python script.
boolean = True

Tips : GetVariableメソッドのオーバーロードについて

実は,GetVariableメソッドには,戻り値の型を型パラメータに指定するオーバーロードがあります.戻り値をハッキリさせたいという場合は,オーバーロード版のメソッドを利用するのもいいかもしれません.

C#で設定された関数をIronPythonで使用する

スクリプトスコープに変数の情報を格納するときはSetVariableメソッドを使います. 引数にはスクリプトで使用するの変数の名前,その変数に代入する値を指定します. 代入する値はobject型なので,何でも代入することができます(実際何でも代入できますが後述). 以下はSetVariableメソッドで数値を代入する例です.

scope.SetVariable("variable", 3);

これだけです. 簡単でしょう? 以上をまとめると,IronPythonで定義された関数をC#で使用するプログラムは以下のようになります.

Console.WriteLine("[ 其の弐(弐) : C#で設定された変数をIronPythonで使用する ]\n");

// スコープとソースを生成
ScriptScope scope2_2 = engine.CreateScope();
ScriptSource source2_2 = engine.CreateScriptSourceFromFile("script2-2.py");

// グローバル変数の設定
scope2_2.SetVariable("number", 3.1415925);
scope2_2.SetVariable("string", "This is a iron python script.");
scope2_2.SetVariable("boolean", true);

// スクリプトの実行
source2_2.Execute(scope2_2);

Console.WriteLine("\n\n");

この部分が実行されたとき,例えばscript2-2.pyに以下のような記述がされているならば

print("number = " + str(number))
print("string = " + string)
print("boolean = " + str(boolean))

コンソールにはこのように出力されるはずです.

[ 其の弐(弐) : C#で設定された変数をIronPythonで使用する ]

number = 3.1415925
string = This is a iron python script.
boolean = True

其の参 : 関数の受け渡し

実は,プログラムとスクリプトの間で,関数も自由に受け渡しすることができます. しかもC#標準ライブラリには,戻り値のないメソッドを表すActionデリゲートや,戻り値のあるメソッドを表すFuncデリゲートが用意されています. 関数の受け渡しをするときは,GetVariableの戻り値をActionFuncにキャストしたり,SetVariableの引数にActionFuncを指定するだけで,関数の受け渡しを容易に行うことができます.

IronPythonで定義された関数をC#で使用する

ではまず,IronPythonで定義された関数をC#で使用するサンプルです.

Console.WriteLine("[ 其の参(壱) : IronPythonで定義された関数をC#で使用する ]\n");

// スコープとソースを生成
ScriptScope scope3_1 = engine.CreateScope();
ScriptSource source3_1 = engine.CreateScriptSourceFromFile("script3-1.py");

// スクリプトの実行(読み込み)
source3_1.Execute(scope3_1);

// 戻り値の持たない関数の実行
Action print_something = scope3_1.GetVariable("print_something");
print_something();

// 戻り値をもつ関数の実行
Func<string> return_something = scope3_1.GetVariable("return_something");
Console.WriteLine(return_something());

Console.WriteLine("\n\n");

この部分が実行されたとき,例えばscript3-1.pyに以下のような記述がされているならば

def print_something():
    print("This function is defined in IronPython.")

def return_something():
    return "Here, this is the return value from IronPython."

コンソールにはこのように出力されるはずです.

[ 其の参(壱) : IronPythonで定義された関数をC#で使用する ]

This function is defined in IronPython.
Here, this is the return value from IronPython.

C#で定義された関数をIronPythonで使用する

次に,今度はC#で定義された関数をIronPythonで使用するサンプルです.

Console.WriteLine("[ 其の参(弐) : C#で定義された関数をIronPythonで使用する ]\n");

// スコープとソースを生成
ScriptScope scope3_2 = engine.CreateScope();
ScriptSource source3_2 = engine.CreateScriptSourceFromFile("script3-2.py");

// 戻り値の持たない関数の設定
scope3_2.SetVariable("print_something", new Action(PrintSomething));

// 戻り値をもつ関数の設定
scope3_2.SetVariable("return_something", new Func<string>(ReturnSomething));

// スクリプトの実行
source3_2.Execute(scope3_2);

Console.WriteLine("\n\n");

但し,PrintSomethingメソッド・ReturnSomethingメソッドは以下のように実装されているものとします.

// 適当に作ったメソッド
public static void PrintSomething()
{
    Console.WriteLine("This method is defined in C#.");
}

// 適当に作ったメソッド其の弐
public static string ReturnSomething()
{
    return "Here, this is the return value from C#.";
}

この部分が実行されたとき,例えばscript3-2.pyに以下のような記述がされているならば

print_something()
print(return_something())

コンソールにはこのように出力されるはずです.

[ 其の参(弐) : C#で定義された関数をIronPythonで使用する ]

This method is defined in C#.
Here, this is the return value from C#.

其の肆 : C#で作成されたインスタンスをIronPythonで使用する

お前そんなことできたんかい!!?? と思う方がいると思います. 其の弐の項では,私はSetVariableメソッドの第に引数にはには何でも指定することができると述べました. これがIronPythonのヤベェところです. Luaスクリプトのスタックが扱うことができる値の型は,nil,数値,真理値,文字列,関数,テーブル,軽量ユーザーデータ,ユーザーデータ,コルーチン用のスレッドに限定されていました. ですがIronPythonでは,C#で作成した任意の型のインスタンスをスクリプトに渡し,スクリプトではそのインスタンスのpublicなメンバにアクセスすることができます. すなわち,C#でオブジェクトのインスタンスをいじるときと同じようなことができます. では実際にそれをやってみましょう.

例えば,ここにクラスがあります.

// 適当に作ったクラス
public class SomeObject
{
    public string Message;

    public void PrintSomething()
    {
        Console.WriteLine("Method \"PrintSomething\" called.");
    }
}

このクラスのオブジェクトをIronPythonに渡します.

Console.WriteLine("[ 其の肆 : C#で作成されたインスタンスをIronPythonで使用する ]\n");

// スコープとソースを生成
ScriptScope scope4 = engine.CreateScope();
ScriptSource source4 = engine.CreateScriptSourceFromFile("script4.py");

// SomeObjectクラスのインスタンスの作成
SomeObject obj = new SomeObject();

// グローバル変数の設定
scope4.SetVariable("some_object", obj);

// スクリプトの実行
source4.Execute(scope4);

// インスタンスのメンバ変数の値を確認
Console.WriteLine(obj.Message);

Console.WriteLine("\n\n");

ここで,script4.pyにはこのような記述があるとします.

some_object.PrintSomething()
some_object.Message = "Member \"Message\" modified."

そしてこれを実行すると……

[ 其の肆 : C#で作成されたインスタンスをIronPythonで使用する ]

Method "PrintSomething" called.
Member "Message" modified.

何ということでしょう,渡されたインスタンスのメソッドを実行したり,メンバ変数の内容を変更することができているではありませんか!!こういった具合に,IronPythonスクリプトには任意の型のインスタンスを渡すことができ,IronPythonスクリプトは渡されたインスタンスを好き勝手にいじることができます. 恐るべし……

其の伍 : コルーチン

IronPythonはLuaと同じようにコルーチンを使って状態遷移を容易に記述することができます.

Pythonでのコルーチン

Pythonでのコルーチンは以下のように記述されます

def coroutine():
    while True:
        message = yield "process stop"
        print(message)
        
generator = coroutine()
print(next(generator))
print(generator.send("Message 1"))
print(generator.send("Message 2"))
print(generator.send("Message 3"))

Pythonでは「ジェネレータ」なるものでコルーチンを実装します. 上の例について,coroutine関数の戻り値は実はこの「ジェネレータ」と呼ばれるものなのです. そして,そのジェネレータに値を送るなどしてコルーチンを実行させます. 今回は,コルーチンの基礎部分をC#に組み込むことを目的としているため,深くは掘り下げません. 詳しくは他のサイトなどをご覧ください.

C#でIronPythonのコルーチンを動かすときは,このジェネレータを取得するところから始まります. まず,IronPythonからコルーチンの実装に相当する部分を関数として受け取ります.

Func<PythonGenerator> coroutine = scope.GetVariable("coroutine");

次に,コルーチンを動かすためのジェネレータをPythonGenerator型として受け取ります.

PythonGenerator generator = coroutine();

ジェネレータを受け取った後にコルーチンを動かすには,nextメソッドを使用します.

generator.next();

またvariable = yield "return value"といったような表記がある場合はsendメソッドを使えばおkです.

以上を踏まえると,コルーチンを利用するプログラムは以下のようになります.

Console.WriteLine("[ 其の伍 : コルーチンを使う ]\n");

// スコープとソースを生成
ScriptScope scope5 = engine.CreateScope();
ScriptSource source5 = engine.CreateScriptSourceFromFile("script5.py");

// スクリプトの実行
source5.Execute(scope5);

// コルーチンの読み出し・実行
Func<PythonGenerator> coroutine = scope5.GetVariable("coroutine");
PythonGenerator generator = coroutine();
generator.next();
generator.next();
generator.next();

Console.WriteLine("\n\n");

この部分が実行されたとき,例えばscript5.pyに以下のような記述がされているならば

# coding: utf-8

def coroutine():
    print("Hi, I am a coroutine.")
    yield
    print("If you call me once, the process stops at \"yield\" statement.")
    yield
    print("And if you call me again, then the process resumes and stops at next \"yield\" statement.")
    yield

コンソールにはこのように出力されるはずです.

[ 其の伍 : コルーチンを使う ]

Hi, I am a coroutine.
If you call me once, the process stops at "yield" statement.
And if you call me again, then the process resumes and stops at next "yield" statement.

スクリプトスコープをうまく利用すると,インスタンスの状態遷移をうまいことコルーチンで制御できるかもしれませんね.

結論,ヤベェ.

C#に組み込む時に使用するIronPythonは,Luaを組み込むときにできることと同じようなことができる上に,スクリプトに任意の型のインスタンスを渡してスクリプト上でいじることもできるので,自由度がLuaよりも遥かに高いです. 今までPythonをdisってきた自分ですが,そんな自分でもぐうの音も出ない位の機能と柔軟性を,IronPythonは持ち合わせています. IronPythonはPythonなので,numpyなどといったpython系のライブラリと組み合わせると,さらに高度な組み込みを行うことができるのではないのかと考えております.

また,IronPythonを使っていて,C#で定義したクラスをIronPythonに見えるようにして,IronPythonでインスタンスを生成することはできないものかと思いました. これができれば,C#とIronPythonによる組み込みスクリプティングは,さらに自由度の高いものとなることでしょう.

余談

IronRubyについて

C#にIronPythonを組み込む手法と同じくして,IronRubyを組み込むこともできます. 既にお分かりかと思いますが,.NETやMonoで動くRubyです. 文法もRubyそのままですです. どのように記述すれば組み込めるかがわかるように,以下にサンプルのソースコードを示します.

using System;

using IronRuby;
using Microsoft.Scripting.Hosting;

namespace RubySample
{
    class MainClass
    {
        public static void Main()
        {
            // Ruby エンジンを生成
            ScriptEngine engine = Ruby.CreateEngine();

            // スコープとソースを生成
            ScriptScope scope = engine.CreateScope();
            ScriptSource source = engine.CreateScriptSourceFromFile("script.rb");

            // グローバル変数を設定
            scope.SetVariable("number1", 3.14159265);
            scope.SetVariable("number2", 2.71828182);
            scope.SetVariable("string", "This is a ruby script.");
            scope.SetVariable("boolean", true);
            scope.SetVariable("some_object", new SomeObject());

            // 実行した後の戻り値を出力
            Console.WriteLine("[ Output from Ruby ]\n");
            source.Execute(scope);

            // グローバル変数の取得
            Console.Write("\n\n\n");
            Console.WriteLine("[ Output from C# ]\n");
            Console.WriteLine(scope.GetVariable("number_ret"));
            Console.WriteLine(scope.GetVariable("string_ret"));
            Console.WriteLine(scope.GetVariable("boolean_ret"));
        }
    }

    // 適当に作ったクラス
    public class SomeObject
    {
        public void PrintSomething()
        {
            Console.WriteLine("Method \"PrintSomething\" called.");
        }
    }
}

例えば,script.rbに以下のような記述があった場合

puts "Hello world from iron ruby!"

puts number1
puts number2
puts string
puts boolean

some_object.PrintSomething()

number_ret = number1 + number2;
string_ret = "This string is set in ruby script.";
boolean_ret = false;

コンソールには以下のように出力される

[ Output from Ruby ]

Hello world from iron ruby!
3.14159265
2.71828182
This is a ruby script.
true
Method "PrintSomething" called.



[ Output from C# ]
5.85987447
This string is set in ruby script.
False

……はずなのですが,上に示したサンプルソースコードだとConsole.WriteLine(scope.GetVariable("number_ret"))の部分で例外が発生します. NumberRet変数が存在しないのだとか. これはIronRubyのバグなのか,それとも自分の書き方が悪かったのか…… もし後者だった場合はご指摘願います.

IronPythonでAltseedが動く…!?

この記事の冒頭で,IronPythonとは.NETやMonoの上で動作するPythonであると述べました..NET版のAltseedはC#,F#,VBから弄ることが可能です. ということは,IronPythonでもAltseed製のゲームが作れるのでは…? と思い,こんなスクリプトを作成しました.

# coding: utf-8

import clr
import sys

# Import DLL
sys.path.append(".")
clr.AddReference("Altseed.dll")
import asd

# Abstract input judgement
def KeyPush(key):
    return asd.Engine.Keyboard.GetKeyState(key) == asd.ButtonState.Push
def KeyHold(key):
    return asd.Engine.Keyboard.GetKeyState(key) == asd.ButtonState.Hold
def KeyRelease(key):
    return asd.Engine.Keyboard.GetKeyState(key) == asd.ButtonState.Release
def KeyFree(key):
    return asd.Engine.Keyboard.GetKeyState(key) == asd.ButtonState.Free

# Define player
class Player(asd.TextureObject2D):

    # Constructer
    def __init__(self):
        self.Texture = asd.Engine.Graphics.CreateTexture2D("Player.png")
        self.CenterPosition = self.Texture.Size.To2DF() / 2
        self.Position = asd.Vector2DF(320, 360)

    # Update player
    def OnUpdate(self):

        # Key input
        if KeyHold(asd.Keys.Up):
            self.Position += asd.Vector2DF( 0, -5)
        if KeyHold(asd.Keys.Down):
            self.Position += asd.Vector2DF( 0,  5)
        if KeyHold(asd.Keys.Left):
            self.Position += asd.Vector2DF(-5,  0)
        if KeyHold(asd.Keys.Right):
            self.Position += asd.Vector2DF( 5,  0)

        # Clamp moving area
        texture_size = self.Texture.Size / 2
        window_size = asd.Engine.WindowSize
        x = asd.MathHelper.Clamp(self.Position.X, window_size.X - texture_size.X, texture_size.X)
        y = asd.MathHelper.Clamp(self.Position.Y, window_size.Y - texture_size.Y, texture_size.Y)
        self.Position = asd.Vector2DF(x, y)

# Initialize altseed
asd.Engine.Initialize("Altseed × IronPython", 640, 480, asd.EngineOption())

# Create scene, layer, and object
scene = asd.Scene()
layer = asd.Layer2D()
player = Player()

# Register them
scene.AddLayer(layer)
layer.AddObject(player)

# ChangeScene
asd.Engine.ChangeScene(scene)

# Update altseed
while asd.Engine.DoEvents():
    asd.Engine.Update()
    if KeyPush(asd.Keys.Escape):
        break

# Terminate altseed   
asd.Engine.Terminate()

MacでMonoをインストールした場合,ipyコマンドでIronPythonスクリプトを実行することができます. これで動くか……? いや,まさかな……

ACAC-2019-12-25(3).png

キィィィェェェァァァァァァァァァァァァァァァァァァウゴイタァァァァァァァァァァァァァァァァァァ

IronPythonは.NETやMono上で動くということもあり,C#とも相性が良く共存させやすい上に,IronPython単体でもゲームが1本作れるだけ強力であるということを実感させられました. この調子でいけば,IronPythonスクリプト上でゲームオブジェクトが自由に作れるといったこともできるかもしれませんね.

終わりに

今回はIronPythonスクリプトをC#で作成したプログラムに組み込む方法を示してきました. いざ使ってみれば,KeraLuaとは比べ物にならないぐらい使いやすかったなと感じました. IronPythonはマイナーな言語ですが,この機会に一度触ってみてはいかがでしょうか. かなり分量の大きな話題となってしまいましたが,初めての組み込みスクリプティングはこれにて終了となります. 最後までお付き合いいただき,ありがとうございました.

今回作成したサンプルソースコードはgithub情で公開しています. 詳しくはしたのリンク先のリポジトリの「Day25」をご参照ください. https://github.com/GCLemon/ACAC2019

SHARE THIS POST