tcl言語とC言語の連携
tcl言語でC言語の関数を使う
tcl言語はプラットフォームを問わず幅広く使われている言語です。今回は、tcl言語からC言語の関数を呼び出す方法について解説します。
tcl言語は高水準のインタープリタなので、柔軟にプログラム開発や試作などを手軽にオンボードで行うことができます。一方、C言語では低水準プログラミングに対応しており、かつ、ライブラリも完備しているので、Linuxの全ての機能を使ったり、ハードウェアの直接アクセスを行ったりすることができます。組込みボードでは低水準のハードウェアアクセスの部分のみをC言語の関数にまとめて、それ以外の高水準のプログラミングをtcl言語で行うのが最適だと思われます。
ここではC言語で簡単な演算を行う関数を作成し、それを共有ライブラリとしてコンパイルをして、tcl言語のスクリプトからC言語で書かれた自作共有ライブラリを呼び出すことをやってみます。
tcl言語から呼び出すC言語関数
全ての引数の数と累積加算の戻り値を返すC言語側の関数は、リスト1のようになります。
tcl言語から呼び出されるC言語で書かれた自作共有ライブラリ内での関数の戻り値は、正常終了を示すTCL_OKとエラー発生を示すTCL_ERRORのいずれかを指定します。tcl言語から呼び出されるC言語で書かれた自作共有ライブラリでは、初期化関数において、tcl言語から呼び出されるC言語の関数を登録します。
初期化関数名は以下となります。
任意の名称は必ずしも共有ライブラリ名と一致させる必要はなく、tcl言語のスクリプトで共有ライブラリを呼び出すときに、共有ライブラリのパスとともに任意の名称を併せて指定をします。
tcl言語から呼び出されるC言語の関数の仕様は、旧仕様と新仕様の2つが併存しますが、現状のSH7706LSR上では新仕様の関数は動作するものの安定さに欠けるので、ここではSH7706LSRで安定動作をする旧仕様で関数を作成します。
tcl言語から呼び出されるC言語の関数を記述する場合は、tcl言語インターフェースのためのヘッダが必要になります。これはリスト1の1行目のようになります。
また、C言語側での関数名はtcl言語から呼び出す関数名である必要はなく、初期化関数においてtcl言語から呼び出す関数名を指定するので、適当なわかりやすい関数名にしておきます。
自作共有ライブラリ内での初期化関数やtcl言語から呼び出されるC言語の関数とtcl言語インタープリタ処理系とのインターフェースは、tcl言語インタープリタ処理系内部のポインタであるTcl_Interp型のポインタを介して行います。
初期化関数においては、tcl言語から呼び出されるC言語の関数の生成とtcl言語から呼び出されるC言語の関数の登録を行います。
tcl言語から呼び出されるC言語の関数の生成は、Tcl_CreateCommand関数(リスト1の6行目)で行い、第1引数ではTcl_Interp型のポインタ、第2引数ではtcl言語から呼び出す関数名、第3引数ではtcl言語から呼び出されるC言語での関数名を指定し、これらの3つの引数は必須となります。オプションとしては第4引数では固有値、第5引数ではコマンド削除時に呼び出される終了処理のためのC言語での関数名です。通常、第4引数での固有値は不要ですが、複数のtcl言語の関数で同じC言語の関数を共有させる場合は、tcl言語の関数の判別に固有値を使います。
tcl言語から呼び出されるC言語の関数の登録は、PkgProvide関数(リスト1の7行目)で行い、第1引数ではTcl_Interp型のポインタ、第2引数ではtcl言語から呼び出す関数名、第3引数ではバージョンを指定しますが、特に必要がなければ適当な値にしておきます。
tcl言語から呼び出されるC言語の関数であるCalcCmd関数(リスト1の11~26行目)では、第1引数では固有値、第2引数ではTcl_Interp型のポインタ、第3引数ではtcl言語関数での引数の数、第4引数では引数本体を受け取ります。
tcl言語から呼び出されるC言語の関数内で数値などの引数の実体を取得するには、引数本体だけでなくTcl_Interp型のポインタと併用しなければならず、数値を取得するにはTcl_GetInt関数(リスト1の20行目)で行います。
C言語での戻り値は TCL_OK または TCL_ERROR のいずれかなので、tcl言語から見た戻り値はTcl_Interp型の resultメンバに文字列ポインタを代入し(リスト1の15行目)、数値の場合であっても数値の文字列に変換して文字列として代入をします(リスト1の24行目)。
Lua言語からのハードウェアアクセス
C言語での記述
前回、Lua言語からC言語の関数を呼び出す方法について説明しましたので、Lua言語からのはハードウェアアクセスでは、C言語の部分でハードウェアアクセスをするコードを記述するだけです。ハードウェアアクセスを行うLua言語から呼び出されるC言語の関数はリスト2のようになります。
リスト2の22~167行目まではハードウェアアクセスを行う関数で、物理メモリから読み込む関数3つと物理メモリへ書き込む関数3つの合計6つの関数となります。これらは外部から呼び出させることはないので、static宣言の関数として記述します。
SHプロセッサではメモリアクセスが1バイトアクセス、2バイトアクセス、4バイトアクセスの3種類があるので、それぞれのメモリアクセスに対応した関数をそれぞれ3つずつ用意する必要があります。基本的にはC言語でハードウェアアクセスを行う処理の記述と同じですが、エラー終了の処理のみは、Lua言語インターフェースの規約に従ったコードとなっています(たとえばリスト2の29~30行目)。lua_pushstring関数でエラーメッセージをセットしてからlua_error関数で即、異常終了させます。他のエラー処理の部分も同様に処理をしています。
外部から呼び出させる関数はmemory関数で、リスト2の169~203行目となります。Lua言語処理系からmemory関数へ渡される引数は2つまたは3つでそれ以外はエラー終了にしています(リスト2の177~180行目)。
第1引数はメモリアクセスのサイズを文字列で受け取り(リスト2の181行目)、その文字列の内容によりハードウェアアクセス行う関数を決めています(リスト2の182~194行目)。
第2引数は物理メモリアドレスでlua_Number型の数値変数で受け取っていますが、ハードウェアアクセス行う関数に対しては unsigned int型に変換して物理メモリアドレスを渡しています。
もし、引数の数が3つの場合は、第3引数で対象となる物理メモリアドレスへセットするlua_Number型の値として受け取り、ハードウェアアクセス行う関数に対しては unsigned int型に変換して物理メモリアドレスへセットする値を渡しています。引数が2つのみの場合は物理メモリを読み込み、その値を戻り値としてセットするのみとなります。
コンパイルと実行
共有ライブラリのコンパイルはPC上で行い、ソースファイル名が memory.c 共有ライブラリ名が memory.so とすると以下のようにコンパイルします。
できあがったmemory.soをSH7706LSRのLua言語インタープリタを実行する場所にコピーをする、とLua言語スクリプトからハードウェアアクセスを行うことができます。また、以下のようにSH7706LSRのLinux上でインタープリタ単体を起動して、コマンド入力でハードウェアアクセスを行うこともできます。
上記の例では、SH7706LSRの付属LEDを点灯させる処理を行っています。インタープリタを抜けるには、Ctrl+Cを押します。
tcl言語からのハードウェアアクセス
C言語での記述
前半でtcl言語からC言語の関数を呼び出す方法について説明しましたので、tcl言語からのはハードウェアアクセスでは、C言語の部分でハードウェアアクセスをするコードを記述するだけです。
ハードウェアアクセスを行うtcl言語から呼び出されるC言語の関数は、リスト3のようになります。
リスト3の20~168行目まではハードウェアアクセスを行う関数で、物理メモリから読み込む関数3つと物理メモリへ書き込む関数3つの合計6つの関数となります。これらは外部から呼び出させることはないので、static宣言の関数として記述します。
SHプロセッサではメモリアクセスが1バイトアクセス、2バイトアクセス、4バイトアクセスの3種類があるので、それぞれのメモリアクセスに対応した関数をそれぞれ3つずつ用意する必要があります。基本的にはC言語でハードウェアアクセスを行う処理の記述と同じですが、エラー終了の処理のみは、tcl言語インターフェースの規約に従ったコードとなっています(たとえばリスト3の27~28行目)。
Tcl_Interp型のresultメンバにエラーメッセージの文字列ポインタを代入し、C言語の戻り値をTCL_ERRORとして戻り、呼び出し元のMemory_Cmd関数でもC言語の戻り値をTCL_ERRORとして戻り、その呼び出し元であるtcl言語処理系でエラーとなります。他のエラー処理の部分も同様に処理をしています。
外部から呼び出させる関数は初期化関数 Memory_Init関数(リスト3の207~211行目)と処理本体の MemoryCmd関数(リスト3の170~205行目)なります。このうち、MemoryCmd関数は static宣言の関数なので直接外部とリンクは不可能ですが、初期化関数であるMemory_Init関数において 処理本体である MemoryCmd関数をtcl言語処理系に対して登録していますので、これ以降、tcl言語処理系から呼び出させることになります。
tcl言語処理系からMemoryCmd関数へ渡される引数は2つまたは3つでそれ以外はエラー終了にしています(リスト3の176~179行目)。第1引数はメモリアクセスのサイズを文字列で受け取り、その文字列の内容によりハードウェアアクセス行う関数を決めています(リスト3の180~192行目)。
第2引数は、Tcl_GetInt関数により物理メモリアドレスを受け取っています(リスト3の193行目)。
もし、引数の数が3つの場合は、第3引数で対象となる物理メモリアドレスへセットする値を受け取っています(リスト3の196行目)。引数は3つの場合のみ物理メモリアドレスへ値をセットし、引数が2つのみの場合は物理メモリを読み込み、その値を戻り値としてセットするのみとなります。
コンパイルと実行
共有ライブラリのコンパイルはPC上で行い、ソースファイル名が memory.c 共有ライブラリ名が memory.so とすると以下のようにコンパイルします。
できあがったmemory.soをSH7706LSRのtcl言語インタープリタを実行する場所にコピーをすると、tcl言語スクリプトからハードウェアアクセスを行うことができます。また、以下のようにSH7706LSRのLinux上でインタープリタ単体を起動して、コマンド入力でハードウェアアクセスを行うこともできます。
上記の例ではSH7706LSRの付属LEDを点灯させる処理を行っています。
次回は
次回はSH7706LSRのLinux上でハードウェア割り込みを扱う方法について解説します。