先日の記事で、.NETを標準ライブラリとして使用できるようにしたと書きましたが、今回その仕様をさらに改良して、任意の外部アセンブリを読み込んで相互運用できるようにしました。それに合わせて仕様が曖昧だったimport文も見直したので、簡単に解説しておこうかなと思います。
まず、以下のようなC#のコードがあったとします。
//In TestInterface.cs using System; using System.Collections.Generic; namespace InteroperabilityTest { public interface TestInterface { void DoSomething(); int GetSomeInt(); List<int> GetIntList(); } } // In InteroperabilityTest.cs using System; using System.Collections.Generic; namespace InteroperabilityTest { public class InteroperabilityTest : TestInterface { public void DoSomething() { Console.WriteLine("Hello from 'DoSomething'"); } public List<int> GetIntList() { Console.WriteLine("GetIntList called"); return new List<int>{1, 2, 3, 4, 5}; } public int GetSomeInt() { Console.WriteLine("GetSomeInt called"); return 100; } } } // In StaticTest.cs using System; using Expresso.Runtime.Builtins; namespace InteroperabilityTest { public class StaticTest { public static void DoSomething() { Console.WriteLine("Hello from StaticTest.DoSomething"); } public static bool GetSomeBool() { Console.WriteLine("GetSomeBool called"); return true; } public static ExpressoIntegerSequence GetSomeIntSeq() { Console.WriteLine("GetSomeIntSeq called"); return new ExpressoIntegerSequence(1, 10, 1, true); } } }
このアセンブリ名がInteroperabilityTest.dllだとして、以下のようなExpressoのコードを記述します。
module main; import InteroperabilityTest.{InteroperabilityTest, StaticTest} from "./InteroperabilityTest.dll" as {InteroperabilityTest, StaticTest}; def main() { let t = InteroperabilityTest{}; t.DoSomething(); let i = t.GetSomeInt(); let list = t.GetIntList(); StaticTest.DoSomething(); let flag = StaticTest.GetSomeBool(); let seq = StaticTest.GetSomeIntSeq(); println(i, list, flag, seq); }
すると、Console.WriteLineの出力をしつつ、100、[1, 2, 3, 4, 5, ...]、true、[1..11:1]という出力がされます。ご覧の通り、普通FFI(Function Foreign Interface)を使用して呼べるのは関数だけですが、インスタンスの生成、インスタンスメソッドの呼び出しも行えます。ランタイム環境が共通になっている恩恵ですね。また、コンパイル後のILの状態ならば、リフレクションの機能を使用してC#などの他の言語から呼び出すこともできます。まさに「無敵」状態です。.NETを採用した強みが出ていますね。
2018/4/7 追記: 以前からプロパティのgetはできたものの、本日、setも実装し、完全にプロパティを扱えるようになりました。完全な互換性を保持するまであとenumを使えるようにするだけと、あと1歩になりました。2018/4/8 追記: ILのenumも参照できるようになりました。これで完全な互換性を持ったことになるはずです。
C#からC++のコードの呼び出しができたはずなので、C#でラップすることで、C++のライブラリもExpressoから呼べることでしょう。
さて、以前のコードと比べてだいぶ様相が変わっているのが、importの箇所でしょう。以前のコードでは、importの直後は文字列になっていましたが、仕様改訂を経てRustとPythonのimportの合いの子のような見た目になりました。import文をEBNFで記述すると、以下のようになっています。
"import" ident [ "::" ( ident | '{' ident { ',' ident } '}' ) ] { '.' ( ident | '{' ident { ',' ident } '}' ) [ "from" string_literal ] "as" ( ident | '{' ident { ',' ident } '}') ';'
つまり、Pythonと違ってas節は省略できません。これは、元の識別子そのままでインポートできないための措置です。インポートした識別子には、必ず別名をつけなければなりません。そうでなければ、"::"や"."を含む名前で識別せねばならず、これはExpressoの仕様上できないからです("::"や"."を含む識別子は定義できない)。import節は、.exsファイルからモジュール内の型を指定する場合に、"::"を使います。これはその他の式でも同様です。
一方、C#で生成したDLLを読む場合には、名前空間から型名を指定してください。また、C#などで生成したDLLを読む場合、変数名や関数名をインポートする識別子に指定することはできません。ILコードではアセンブリレベルにフィールドやメソッドを定義できない制約のためです。
from節のファイル名は、記述されているソースファイルに対して相対パスになります。つまり、上のコードでInteroperabilityTest.dllは、このソースファイルと同じディレクトリに存在します。この挙動は、他の.exsファイルを読み込む時も同じです。
こうして、Expressoから自由にC#のコードも呼べるようになったことですし、そろそろセルフホスティングだか、セルフブートストラップだかも視野に入れていきたい・・・
0 件のコメント:
コメントを投稿
なにか意見や感想、質問などがあれば、ご自由にお書きください。