Mercurial > hg > Papers > 2021 > anatofuz-master
view paper/chapter/05-perl.tex @ 113:b804a51037c7
final?
author | anatofuz <anatofuz@cr.ie.u-ryukyu.ac.jp> |
---|---|
date | Sat, 06 Feb 2021 17:18:46 +0900 |
parents | 8f0ff6d552ed |
children | 9ba02bf8a09f |
line wrap: on
line source
\chapter{トランスパイラによるメタ計算} GearsOSはCbCで実装を行う。 CbCはC言語よりアセンブラに近い言語である。 すべてを純粋なCbCで記述すると記述量が膨大になる。 またノーマルレベルの計算とメタレベルの計算を、全てプログラマが記述する必要がでる。 メタ計算では値の取り出しなどを行うが、 これはノーマルレベルのCodeGearのAPIが決まれば一意に決定される。 したがってノーマルレベルのみ記述すれば、 機械的にメタ部分の処理は概ね生成可能となる。 また、メタレベルのみ切り替えたいなどの状況が存在する。 ノーマルレベル、メタレベル共に同じコードの場合は記述の変更量が膨大であるが、 メタレベルの作成を分離するとこの問題は解消される。 GearsOSではメタレベルの処理の作成にPerlスクリプトを用いており、 ノーマルレベルで記述されたCbCから、 メタ部分を含むCbCへと変換する。 変換前のCbCをGearsCbCと呼ぶ。 \section{トランスパイラ} プログラミング言語から実行可能ファイルやアセンブラを生成する処理系のことを、一般的にコンパイラと呼ぶ。 特定のプログラミング言語から別のプログラミング言語に変換するコンパイラのことを、 トランスパイラ、トランスコンパイラ、トランスラなどと呼ぶ。 本論文では以下トランスパイラと呼ぶ。 トランスパイラとしてはJavaScriptを古い規格のJavaScriptに変換するBabel\cite{babel}がある。 またトランスパイラは、変換先の言語を拡張した言語の実装としても使われる。 JavaScriptに強い型制約をつけた拡張言語であるTypeScriptは、 TypeScriptから純粋なJavaScriptに変換を行うトランスパイラである。 すべてのTypeScriptのコードはJavaScriptにコンパイル可能である。 JavaScriptに静的型の機能を取り込みたい場合に使われる言語であり、 JavaScriptの上位の言語と言える。 GearsOSはCbCにノーマルレベル、メタレベルの書き別けの機能などを追加した拡張言語であると言える。 コンパイル時にCMakeによって呼び出される2種類のPerlスクリプトで等価な純粋なCbCに変換される。 これらのPerlスクリプトはGearsOSのCbCから純粋なCbCへと変換している為に一種のトランスパイラと言える。 \section{トランスパイラによるメタレベルのコード生成} トランスパイラはノーマルレベルで記述されたGearsOSを、メタレベルを含むCbCへと変換する役割である。 変換時に様々なメタ情報をCbCのファイルに書き出すことが可能である。 従来はStubの生成や、 引数の変更などを行っていたが、 さらにメタレベルのコードをトランスパイラで作製したい。 トランスパイラ上でメタレベルのコードを作製することによって、 GearsOS上でのアプリケーションの記述が容易になり、かつメタレベルのコードを柔軟に扱うことができる。 本研究では様々なメタレベルのコードを、トランスパイラで生成することを検討した。 \section{トランスパイラ用のPerlライブラリ作製}\label{sec:perlLib} 従来のPerlトランスパイラはgenerate\_stub.plとgenerate\_context.plの2種類のスクリプトで構築されていた。 これらのスクリプトはそれぞれ独立した処理を行っていた。 しかし本研究を進めるにつれて、 Interfaceのパーサーやメタ計算部分の操作を行うAPIなど、 Perlスクリプトで共通した実装が見られた。 さらにgenerate\_stub.plらPerlスクリプトの行数や処理の複雑度が上がり、 適切に処理をモジュール化する必要が生じた。 この為新しく実装したPerlトランスパイラが利用するAPIは、 Perlのモジュール機能を利用しモジュールの形で実装した。 以下に実装したモジュールファイルと、その概要を示す。 \begin{itemize} \item \texttt{Gears::Context} \begin{itemize} \item context.hの自動生成時に呼び出されるモジュール \item 変換後のCbCのコードを解析し、使用されているDataGearの数え上げを行う \end{itemize} \item \texttt{Gears::Interface} \begin{itemize} \item InterfaceおよびImplementのパーサー \end{itemize} \item \texttt{Gears::Template} Gears::Template以下はPerlスクリプトが生成する際に、テンプレートとして呼び出すファイルの定義などがある \begin{itemize} \item \texttt{Gears::Template::Context} \begin{itemize} \item context.hのテンプレート \end{itemize} \item \texttt{Gears::Template::Context::XV6} \begin{itemize} \item CbC Xv6 専用のcontext.hのテンプレート \end{itemize} \item \texttt{Gears::Template::Gmain} \begin{itemize} \item GearsOS Main関数のテンプレート \end{itemize} \end{itemize} \item \texttt{Gears::Stub} \begin{itemize} \item Stub Code Gear生成時に呼び出されるモジュール \end{itemize} \end{itemize} これらはgenerate\_stub.plおよびgenerate\_context.plおよび、本研究で作製したPerlのツールセットからも呼び出される。 \section{context.hの自動生成}\label{sec:context.hGen} GearsOSのContextの定義はcontext.hにある。 ContextはGearsOSの計算で使用されるすべてのCodeGear、 DataGearの情報を持っている。 context.hではDataGearに対応する\texttt{union Data}型の定義も行っている。 Data型はCの共用体であり、 Dataを構成する要素として各DataGearがある。 各DataGearは構造体の形で表現されている。 各DataGear自体の定義もcontext.hのunion Dataの定義の中で行われている。 DataGearの定義はInterfaceファイルで行っていた。 InterfaceファイルはGearsOS用に拡張されたシンタックスのヘッダファイルを使っており、 直接CbCからロードすることができない。 その為従来はプログラマが静的にInterfaceファイルをCbCの文脈に変換し、 context.hに構造体に変換したものを書いていた。 この手法では手書きでの構築のために自由度は高かったが、 GearsOSの例題によっては使わないDataGearも、 context.hから削除しない限りcontextに含んでしまう問題があった。 さらにInterfaceファイルで定義した型をcontext.hに転記し、それをもとにImplの型を考えてCbCファイルを作製する必要があった。 これらをすべてユーザーが行うと、ファイルごとに微妙な差異が発生したりとかなり煩雑な実装を要求されてしまう。 DataGearの定義はInterfaceファイルを作製した段階で決まり、 使用しているDataGear、CodeGearはコンパイル時に確定する。 使用している各Gearがコンパイル時に確定するならば、 コンパイルの直前に実行されるPerlトランスパイラでもGearの確定ができるはずである。 ここからcontext.hをコンパイルタイミングでPerlスクリプト経由で生成する手法を考案した。 \subsection{ビルド時のcontext.hの生成タイミング} context.hをビルドの途中で生成するには、CMakeがcontext.hを作製するようにプログラミングする必要がある。 しかしCMakeの文法はきわめて複雑であるので、現状のGearsOSのCMakeLists.txtの定義を変更したくない。 この為には現在ビルド時に動作するgenerate\_stub.plかgenerate\_context.plのいずれかで生成を行いたい。 GearsCbCからメタ計算を含むCbCファイルに変換するgenerate\_stub.plは各CbCファイルを1つ1つ呼び出していた。 context.hを生成しようとする場合、 プロジェクトで利用する全CbCファイルを扱う必要がある。 従ってgenerate\_stub.plではcontext.hの作製はできない。 Contextの初期化ルーチンを作製するgenerate\_context.plは、その特性上すべてのCbCファイルをロードしていた。 したがってcontext.hを作製する場合はこのスクリプトで行うと現状のCMakeに手をつけずに変更ができる。 context.hを作製するため、generate\_context.plの処理は図\ref{fig:generate_context_2}の処理を実行することになる。 \begin{figure}[h] \begin{center} \includegraphics[width=100mm]{drawio/generate_context.pdf} \end{center} \caption{generate\_context.plを使ったcontext.hとファイル生成} \label{fig:generate_context_2} \end{figure} \subsection{context.hの生成処理} generate\_context.plでcontext.hを作製したい。 この為には、プロジェクトで使用しているDataGearを特定する必要がある。 generate\_context.plの時点でCbCファイルはメタ情報を含む表現に変換されている。 Interfaceの使用と、実装を示す\texttt{\#interface}や\texttt{\#impl}構文はこの時点では落とされている為、別の情報から使用しているDataGrarを取得する必要がある。 取得からcontext.hの作製までの流れを図\ref{fig:generate_context_3}に示す。 DataGearはGearsOSのメタレベルでは構造体で表現されていた。 また、DataGearは各CodeGearの引数の形で受け渡されている。 ここからDataGearを取得するには、すべてのCodeGearの引数をチェックし、構造体の名前を取得すればいい。 すべてのCbCファイルを調査後、 取得した構造体の名前に対応するヘッダファイルがあるかどうかを調査する。 この調査はGears::Contextモジュールで行う。 ヘッダファイルがあった場合、 Gears::InterfaceのAPIを利用して、 InterfaceもしくはImplのファイルとして利用可能であるかを確認する。 Interface、Implファイルでなかった場合は、ただの構造体であるのでcontext.hに含む情報からは除外する。 収集されたDataGearはソースコード\ref{src:queueTree}に示すPerlの連想配列に変換される。 連想配列は、最初のキーがInterfaceの名前であり、 Implementがある場合はInterfaceに対応する値のvalueに、implという連想配列が入れ子で入っている。 この中にInterfaceを実装しているImplの型情報が記録されている。 \lstinputlisting[label=src:queueTree, caption=context.hに出力するDataGearの集合]{src/queueTree.pl} \begin{figure}[htbp] \begin{center} \includegraphics[width=130mm]{drawio/geneConst1.pdf} \end{center} \caption{DataGearの収集とcontext.h生成の処理イメージ} \label{fig:generate_context_3} \end{figure} 作製されたcontext.hのunion Dataの定義をソースコード\ref{src:genData}に示す。 context.hのunion Dataは、アルファベット順でソートされ、 Interface、Implの順で記述される。 ここにはデバッグ用に変換した定義ファイルのパスがコメントで埋め込まれる。 また、多重include防止用のマクロも生成される。 \lstinputlisting[label=src:genData, caption=生成されたcontext.hのunion Data定義]{src/genData.h} \subsection{Interface定義のincludeファイルの解決} DataGearの定義を自動でcontext.hに生成することが可能となった。 ここでDataGearが別のヘッダファイルに定義している構造体などをフィールドで持つケースを扱う。 別のヘッダファイルで定義している構造体を使う場合、context.hの中でDataGearに対応する構造体の定義をする前までに、includeする必要が発生する。 ヘッダファイルとしてstate\_db.hをincludeする宣言が書かれたWorker Intertfaceの実装のmcWorker(ソースコード\ref{src:mcWorker})がある。 generate\_context.plでは、DataGearの定義ファイルをGears::InterfaceのパースAPIでパースし、情報を取得していた。 \lstinputlisting[label=src:mcWorker, caption=mcWorker Implの定義]{src/MCWorker.h} パースした結果の情報含まれるincludeする必要があるヘッダファイルの一覧を取得し、context.hを生成するタイミングで、 struct Contextの定義の前にincludeするコードを挿入する。(ソースコード\ref{src:includeContext}) includeの結果では、 3、5、7行目にそれぞれincludeするマクロが生成されている。 各行の上の行には、 includeの記述があったDataGearのファイルパスが記載される。 \lstinputlisting[label=src:includeContext, caption=context.h内でのinclude]{src/includeContext.h} \subsection{context.hのテンプレートファイル} Perlのモジュールとして\texttt{Gears::Template::Context}を作製した。 xv6プロジェクトの場合は一部ヘッダファイルに含める情報が異なるため、 xv6のビルド用にサブモジュールとして\texttt{Gears::Template::Context::XV6}も実装した。 これらのテンプレートモジュールはgenerate\_context.plの実行時のオプションで選択可能である。 呼び出しにはPerlの動的モジュールロード機能を利用している。 各モジュールに共通のAPIを記述しており、 プロジェクトごと使うテンプレートに限らず共通して呼び出すことが可能である。 \section{meta.pmによるメタ計算部分の入れ替え} \label{sec:metapm} GearsOSでは次のCodeGearに移行する前のMetaCodeGearとして、 デフォルトでは\texttt{\_\_code meta}が使われている。 \texttt{\_\_code meta}はcontextに含まれているCodeGearの関数ポインタを、 enumからディスパッチして次のStub CodeGearに継続するものである。 例えばモデル検査をGearsOSで実行する場合、 通常のStub CodeGearのほかに状態の保存などを行う必要がある。 この状態の保存に関する一連の処理は明らかにメタ計算であるので、 ノーマルレベルのCodeGearではない箇所で行いたい。 ノーマルレベル以外のCodeGearで実行する場合は、 通常のコード生成だとStubCodeGearの中で行うことになる。 StubCodeGearは自動生成され、 基本はContextからのDataGearの取り出しを行う。 これ以外のことを行う場合は、 DataGearの取り出しを含めてメタ計算を自分で実装する必要がある。 しかしモデル検査に関する処理は様々なCodeGearの後に行う必要があるため、 すべてのCodeGearのStubを静的に実装するのは煩雑である。 これを避けるには、 Stub以外のMeta Code Gearをユーザーが自由に定義でき、任意にそのMetaCodeGearに継続できる必要がある。 従来のGearsOSでは、継続先のMetaCodeGearは\texttt{\_\_code meta}に決め打ちとなっているために、 自在に変更するAPIが必要となる。 \subsection{\_\_ncodeによる自由なMetaCodeGearの定義} ユーザーが自由にMetaCodeGearを定義できるAPIとして\texttt{\_\_ ncode}を定義した。 これはCbCのマクロで\texttt{\_\_code}になるように制御されている。 generate\_stub.pl、generate\_context.plの両方のPerlトランスパイラは、 \texttt{\_\_code}で定義されているCodeGerはノーマルレベルのCodeGearだと解釈する。 ノーマルレベルのCodeGearはStubの生成や、メタレベルの情報を含んだものに変換される。 対して\texttt{\_\_ncode}は、MetaCodeGearであると判断されるので、これらの変換が行われない。 ユーザーはメタレベルの計算をすべて実装する必要はあるものの、\texttt{\_\_ncode}を使うと自由にMetaCodeGearを定義できる。 ソースコード\ref{src:mcMeta}は\texttt{\_\_ncode}によって定義した、 モデル検査用のMetaCodeGearである。 \lstinputlisting[label=src:mcMeta, caption=\_\_ncodeによって定義されたMetaCodeGear]{src/mcMeta.cbc} \subsection{meta.pm} ノーマルレベルのCodeGearの処理の後に、StubCodeGear以外のMeta Code Gearを実行したい。 Stub Code Gearに直ちに遷移してしまう\texttt{\_\_code meta}以外のMeta CodeGearに、 特定のCodeGearの計算が終わったら遷移したい。 このためには、特定のCodeGearの遷移先のMetaCodeGearをユーザーが定義できるAPIが必要となる。 このAPIを実装すると、ユーザーが柔軟にメタ計算を選択することが可能となる。 これはいわゆるリフレクション処理に該当する。 GearsOSのビルドシステムのAPIとして\texttt{meta.pm}を作製した。 これはPerlのモジュールファイルとして実装した。 meta.pmはPerlで実装されたGearsOSのトランスパイラであるgenerate\_stub.plから呼び出される。 meta.pmの中のサブルーチンである\texttt{replaceMeta}に変更対象のCodeGearと変更先のMetaCodeGearへのgotoを記述する。 ユーザーはmeta.pmのPerlファイルをAPIとしてGearsOSのトランスパイラにアクセスすることが可能となる。 具体的な使用例をコード\ref{src:metapm}に、 この使用例でのCodeGearの継続の様子を図\ref{fig:metapm}に示す。 meta.pmはサブルーチン\texttt{replaceMeta}が返すリストの中に、特定のパターンで配列を設定する。 各配列の0番目には、goto metaを置換したいCodeGearの名前を示すPerl正規表現リテラルを入れる。 コード\ref{src:metapm}の例では、\texttt{PhilsImpl}が名前に含まれるCodeGearを指定している。 すべてのCodeGearのgotoの先を切り替える場合は\texttt{qr/.*\//}などの正規表現を指定する。 \lstinputlisting[label=src:metapm, caption=meta.pm]{src/meta.pm} \begin{figure}[h] \begin{center} \includegraphics[width=150mm]{drawio/metaPM.pdf} \end{center} \caption{meta.pmを使った継続先のMetaCodeGearの切り替え} \label{fig:metapm} \end{figure} generate\_stub.plはGears CbCファイルの変換時に、 CbCファイルがあるディレクトリにmeta.pmがあるかを確認する。 meta.pmがある場合はモジュールロードを行う。 meta.pmがない場合はmeta Code Gearにgotoするものをデフォルト設定として使う。 これらの処理はPerlのクロージャの形で表現しており、 トランスパイラ側では共通のAPIで呼び出すことが可能である。 各Gode Gearが\texttt{goto文}を呼び出したタイミングでreplaceMetaを呼び出し、 ルールにしたがってgoto文を書き換える。 変換するCodeGearがルールになかった場合は、 デフォルト設定が呼び出される。 ソースコード\ref{src:noreplaceMeta}に、meta.pmの設定を書かなかった場合の変換結果を示す。 ソースコード\ref{src:replaceMcMeta}では、meta.pmの設定を置いた場合の変換結果である。 継続先がmetaからモデル検査用のMetaCodeGearであるmcMetaに切り替わっていることが解る。 \lstinputlisting[label=src:noreplaceMeta, caption=通常のthinkingPhilsImplのメタレベルのコード]{src/nonreplaceMcMeta.cbc} \lstinputlisting[label=src:replaceMcMeta, caption=meta.pmによってmcMetaへと継続が切り替わったthinkingPhilsImpl]{src/replaceMcMeta.cbc} \section{別Interfaceからの書き出しを取得する必要があるCodeGear}\label{sec:interfaceInput} 従来のMetaCodeGearの生成では、 別のInterfaceからの入力を受け取るCodeGearのStubの生成に問題があった。 具体的なこの問題が発生する例題をソースコード\ref{src:insertTest1}に示す。 \lstinputlisting[label=src:insertTest1, caption=別Interfaceからの書き出しを取得するCodeGearの例]{src/pop2test.cbc} この例では\texttt{pop2Test}Code Gearから \texttt{stack->pop2}を呼び出し、 継続として\texttt{pop2Test1}を渡している。 \texttt{pop2Test}自体はStackTest Interfaceであり、 \texttt{stack->pop2}の\texttt{stack}はStack Interfaceである。 例題ではStack Interfaceの実装はSingleLinkedStackである。 SingleLinkedStackの\texttt{pop2}の実装をソースコード\ref{src:pop2}に示す。 \lstinputlisting[label=src:pop2, caption=SingleLinkedStackのpop2]{src/pop2.cbc} pop2はスタックから値を2つ取得するAPIである。 pop2の継続は\texttt{next}であり、 継続先に\texttt{data}と\texttt{data1}を渡している。 data、 data1は引数で受けている\texttt{union Data*}型の変数であり、 それぞれstackの中の値のポインタを代入している。 この操作でstackから値を2つ取得している。 このコードをgenerate\_stub.pl経由でメタ計算を含むコードに変換する。 変換した先のコードを\ref{src:pop2meta}に示す。 \lstinputlisting[label=src:pop2meta, caption=SingleLinkedStackのpop2のメタ計算]{src/pop2meta.cbc} 実際は\texttt{next}は\texttt{goto meta}に変換されてしまう。 data、data1は\texttt{goto meta}の前にポインタ変数\texttt{O\_data}が指す値にそれぞれ書き込まれる。 \texttt{O\_data}はpop2のStub CodeGearである\texttt{pop2SingleLinkedStack\_stub}で作製している。 つまり\texttt{O\_data}はcontext中に含まれているStack Interfaceのデータ保管場所にある変数dataのアドレスである。 \texttt{pop2}のAPIを呼び出すと、 Stack Interface中の\texttt{data}にStackに保存されていたデータのアドレスが書き込まれる。 当初Perlスクリプトが生成した\texttt{pop2Test1}のstub CodeGearはソースコード\ref{src:pop2stub-origin}のものである。 CodeGear間で処理されるデータの流れの概要図を図\ref{fig:stackTest1}に示す。 \lstinputlisting[label=src:pop2stub-origin, caption=生成されたStub]{src/pop2stub-origin.cbc} \texttt{\_\_code pop2Test}で遷移する先のCodeGearはStackInterfaceであり、 呼び出しているAPIは\texttt{pop2}である。 pop2で取り出したデータは、 上記で確認した通りContext中のStack Interfaceのデータ格納場所に書き込まれる。 しかしソースコード\ref{src:pop2stub-origin}の例では\texttt{Gearef(context, StackTest)}でContext中の\texttt{StackTest} Interfaceのdataの置き場所から値を取得している。 これはInterfaceのImplのCodeGearは、Interfaceから値を取得するというGearsOSのルールの為である。 現状ではpop2でせっかく取り出した値をStubCodeGearで取得できない。 ここで必要となってくるのは、 実装しているInterface以外の呼び出し元のInterfaceからの値の取得である。 今回の例ではStackTest InterfaceではなくStack Interfaceからdata、 data1を取得したい。 どのInterfaceから呼び出されているかは、 コンパイルタイムには確定できるのでPerlのトランスパイラでStub Codeを生成したい。 \begin{figure}[h] \begin{center} \includegraphics[width=130mm]{drawio/stackTest1.pdf} \end{center} \caption{stackTest1のstubの概要} \label{fig:stackTest1} \end{figure} 別Interfaceから値を取得するには別の出力があるCodeGearの継続で渡されたCodeGearをまず確定させる。 今回の例では\texttt{pop2Test1}が該当する。 このCodeGearの入力の値と、 出力があるCodeGearの出力を見比べ、 出力をマッピングすれば良い。 Stack Interfaceのpop2はdataとdata1に値を書き込む。 pop2Test1の引数はdata, data1, stackであるので、前2つにpop2の出力を代入したい。 Contextから値を取り出すのはメタ計算であるStub CodeGearで行われる。 別Interfaceから値を取り出そうとする場合、 すでにPerlトランスパイラが生成しているStubを書き換える方法も取れる。 しかしStubCodeGearそのものを、 別Interfaceから値を取り出すように書き換えてはいけない。 これは別Interfaceの継続として渡されるケースと、 次のgoto先として遷移するケースがあるためである。 前者のみの場合は書き換えで問題ないが、 後者のケースで書き換えを行ってしまうとStubで値を取り出す先が異なってしまう。 どのような呼び出し方をしても対応できるようにするには、 Stubを別に別ける必要がある。 GearsOSでは継続として渡す場合や、 次のgoto文で遷移する先のCodeGearはノーマルレベルではenumの番号として表現されていた。 enumが降られるCodeGearは、厳密にはCodeGearそのものではなくStub CodeGearに対して降られる。 StubCodeGearを実装した分だけenumの番号が降られるため、 \texttt{goto meta}で遷移する際にenumの番号さえ合わせれば独自定義のStubに継続させることが可能である。 別Interfaceから値を取り出したいケースの場合、 取り出してくる先のInterfaceと呼び出し元のCodeGearが確定したタイミングで別のStubCodeGearを生成する。 呼び出し元のCodeGearが継続として渡すStubCodeGearのenumを、独自定義したenumに差し替えることでこの問題は解決する。 この機能をPerlのトランスパイラである\texttt{generate\_stub.pl}に導入した。 \section{別Interfaceからの書き出しを取得するStubの生成} 別Interfaceからの書き出しを取得する場合、 generate\_stub.plでは次の点をサポートする機能をいれれば実現可能である。 \begin{itemize} \item goto先のCodeGearが出力を持つInterfaceでかつ継続で渡しているCodeGearが別Interfaceの場合の検知 \begin{itemize} \item この場合はgotoしている箇所で渡している継続のenumを、新たに作製したstubのenumに差し替える \end{itemize} \item 継続で実行された場合に別にInterfaceから値をとってこないといけないCodeGear自身 \begin{itemize} \item Stubを別のInterfaceから値をとる実装のものを別に作製する \end{itemize} \end{itemize} \texttt{generate\_stub.pl}内では変換対象のCbCのソースコードを2度読み込む。 最初の読み込み時に継続の状況を確認し、 2度目の読み込み時に状況を踏まえてコードを生成すれば良い。 初回の読み込み時にInterface経由の\texttt{goto}文があった場合に、別Interfaceからの出力があるかなどの情報を確認したい。 \subsection{初回CbCファイル読み込み時の処理} Interface経由でのgoto文は\texttt{goto interface->method()}の形式で呼び出される。 ソースコード\ref{src:parsedOutputStub.pl}はこの形式で来ていた行を読み込んだタイミングで実行される処理である。 \lstinputlisting[label=src:parsedOutputStub.pl, caption=goto時に使用するinterfaceの解析]{src/parsedOutputStub.pl} 1行目の正規表現はInterface経由でのgoto文の正規表現パターンである。 変数\texttt{\$instance}はInterfaceのインスタンスである。正規表現パターンでは\texttt{interface->method}の\texttt{->}の前に来ている変数名に紐づけられる。 変数\texttt{\$method}はgoto先のInterfaceのAPIである。正規表現パターンでは\texttt{interface->method}の\texttt{->}の後に来ているAPI名である。 ソースコード\ref{src:insertTest1}の\texttt{pop2Test}では、 \texttt{stack->pop2}の呼び出しをしているため、 \texttt{stack}がインスタンスであり、 \texttt{pop2}がAPIである。 現在解析しているgoto文が含まれているCodeGearの名前は、変数\texttt{\$currentCodeGear}で別途保存している。 連想配列である\texttt{\$codeGearInfo}の中には、 各CodeGearで使われている変数と変数の型などの情報が格納されている。 ソースコード\ref{src:parsedOutputStub.pl}の9行目では、 \texttt{\$codeGearInfo}経由でInterfaceのインスタンスから、具体的にどの型が呼ばれているかを取得する。 \texttt{pop2Test}では、 インスタンス\texttt{stack}に対応する型名は\texttt{Stack}と解析される。 ソースコード\ref{src:parsedOutputStub.pl}の10行目で実行されている\texttt{findExistsOutputDataGear}はgenerate\_stub.pl内の関数である。 これはInterfaceの名前とメソッド名を与えると、 Interfaceの定義ファイルのパース結果から出力の有無を確認する動きをする。 出力がある場合は出力している変数名の一覧を返す。 ソースコード\ref{src:insertTest1}の例では\texttt{pop2}は\texttt{data}と\texttt{data1}を出力している為、 これらがリストとして関数から返される。 出力がない場合は偽値を返すために13行目からのif文から先は動かない。 出力があった場合はgenerate\_stub.plの内部変数に出力する変数名と、 Interfaceの名前の登録を行う。 生成するStubは命名規則は、 Stubの本来のCodeGearの名前の末尾に\texttt{\_}に続けて数値をいれる。 \texttt{\_\_code CodeGearStub}の場合は、 \texttt{\_\_code CodeGearStub\_1}となる。 変換するCodeGearは、別のCodeGearのAPI呼び出しによって別に変換される可能性がある。 この変換を切り分けたいため、一意のIDとして数値を入れている。 27行目で\texttt{\$generateHaveOutputStub}のlist要素に現在のCodeGearの名前と、 出力に関する情報を代入している。 現在のCodeGearの名前を保存しているのは、この後のコード生成部分でenumの番号を切り替える必要があるためである。 ソースコード\ref{src:insertTest1}の例では\texttt{pop2Test}が使うenumを書き換える必要がある為、 ここの\texttt{\$currentCodeGear}はpop2Testとなる。 ここで作製した\texttt{\$outputStubElem}は、返還後のCbCコードを生成しているフェーズで呼びされる。 \subsection{enumの差し替え処理} ソースコード\ref{src:generatePickNext.pl}の箇所は遷移先のenumをPerlスクリプトで生成し、 GearsOSが実行中にenumをcontextに書き込むコードを生成するフェーズである。 \lstinputlisting[label=src:generatePickNext.pl, caption=Gearefのコード生成部分]{src/generatePickNext.pl} if文で条件判定をしているが、前者は出力があるケースかどうかのチェックである。 続く条件式はGearsOSのビルドルールとして静的に書いたstubの場合は変更を加えない為に、 静的に書いているかどうかの確認をしている。 変数\texttt{\$pick\_next}で継続先のCodeGearの名前を作製している。 CodeGearの名前は一度目の解析で確認した継続先に\texttt{\_}の後ろにIDをつけたものを結合している。 ここで作製したCodeGearの名前を、3行目でcontextに書き込むCbCコードとして生成している。 実際に生成された例題をソースコード\ref{src:replaceenum}に示す。 \lstinputlisting[label=src:replaceenum, caption=enumの番号が差し替えられたCodeGear]{src/replaceenum.cbc} \subsection{対応するStub Code Gearの作製} enumの番号に対応するStub CodeGearを次は作製する必要がある。 StubCodeGearはすでに作製されているオリジナルのStubCodeGearの中身のうち、 OutputDataGearの取得をしている箇所を、別Interfaceから取得するように変更する必要がある。 Perlスクリプトの文字列置換を使ってこの方法を実装する。 generate\_stub.plは、スクリプトでStubCodeGearの中身を文字列として作製し、CbCファイルに書き出していた。 まず、 StubCodeGearを複製する必要があるため、 中身の文字列を保存するように修正した。 CbCファイルのすべての行を読み込み、ファイルの変換が終了したタイミングで、 enumの差し替えが行われていた場合に差し替えようのStubCodeGearの作製を行う。 実際に行っている箇所を、ソースコード\ref{src:generateOtherStub}に示す。 この処理では、新たに作成するべきStubCodeGearの名前を変数\texttt{\$createStubName}に5行目で代入している。 StubCodeGearの名前は、1度目の読み込み時に作製されたものである。 変換対象の変数と、取得するべきInterfaceの組は、連想配列\texttt{\$replaceArgs}に設定されている。 StubCodeGearの中身は\texttt{\$replaceStubContent}に7行目で代入し、これを\texttt{\$replaceArgs}の変数とInterfaceの組の分だけ置換する。 \lstinputlisting[label=src:generateOtherStub, caption=StubCodeGearの生成箇所]{src/generateOtherStub.pl} ソースコード\ref{src:replaceStubCode}で\texttt{pop2Test1StackTestImpl3}の本体と、StubCodeGearを確認する。 \texttt{pop2...Impl3\_stub}はStackTest Interfaceからすべての値を取得している。 対して\texttt{pop2...Impl3\_1\_stub}は、 Stack Interfaceの継続で渡される為に、値をStackから取り出す必要がある。 見るとdata、dta1の値を、 Gearefマクロを通してStack Interfaceから取得するように変更されている。 どちらのStubも継続先は\texttt{pop2Test1StackTestImpl3}であり、引数で渡す値の型と個数は揃っているために、実装側の変更をする必要がない。 これによって柔軟なメタ計算の生成が可能となった。 \lstinputlisting[label=src:replaceStubCode, caption=生成されたStubCodeGearと、もとのCodeGear]{src/replaceStubCode.cbc} \section{ジェネリクスのサポート} \label{sec:generics} 型の安全性を保ったまま、柔軟な関数の定義を可能とする機能にジェネリクスがある。 ジェネリクスは、型自体を変数(型変数)と設定し、あらゆる型でも同様のふるまいを行うという機能である。 GearsOSではInterface宣言に使われるType、Implなどの型キーワードは、そもそもジェネリクスを意識して作られていた。 このジェネリクスをGearsOSにサポートしたい。 ジェネリクスは型変数を利用して関数、クラスを宣言する部分と、型変数に具体的な値を代入して操作する部分に意味が分離できる。 GearsOSでは関数、クラス宣言の部分は、Interfaceの宣言およびImplmenetの実装に該当する。 型変数に具体的な値をいれる部分は、 Interfaceを利用する場所、もしくはDataGearとして使う場所に相当する。 \subsection{ジェネリクスを使ったInterfaceの定義} ジェネリクスを使ってInterfaceを定義した例を、ソースコード\ref{src:AtomicT}に示す。 GearsOSでジェネリクスを扱う場合は、型名の宣言に続く\texttt{<>}の部分に、型変数を記述する。 この例では、型変数としてTを使用している。 型変数Tがどのような値を使うかは、ジェネリクスを呼び出している箇所で確定する。 \lstinputlisting[label=src:AtomicT, caption=ジェネリクスを使ったAtomicTの定義]{src/AtomicT.h} \subsection{ジェネリクスを使ったImplの定義} Implementの定義でも、ジェネリクスは同様に使うことが可能である。 AtomicTの実装であるAtomicTImplの定義を、ソースコード\ref{src:AtomicTImpl}に示す。 Interfaceでジェネリクスを使った場合、Impl側でもジェネリクスが伝搬される。 この場合は型変数が、Interfaceと同様のTであるので、Interfaceで決定されたTの型と同様の型に決まる。 \lstinputlisting[label=src:AtomicTImpl, caption=ジェネリクスを使ったAtomicTの実装の定義]{src/AtomicTImpl.h} \subsection{ジェネリクスを使ったCodeGearの記述} Implを定義したので対応するCbCファイルを確認する。 ソースコード\ref{src:AtomicTImplCbC}は、ジェネリクスを使ったCodeGearの実装の一部である。 型変数を使ったCodeGearの記述では、型変数は通常の型のように記述できる。 今回はTが型変数であるため、型Tを通常の構造体の型のように見て実装することが可能である。 ただし、 型変数を含むInterfaceおよびImplは、名前の後ろに\texttt{<T>}のように型変数を付け加える必要がある。 \lstinputlisting[label=src:AtomicTImplCbC, caption=ジェネリクスを使ったAtomicTの実装]{src/AtomicTImpl.cbc} \subsection{DataGear定義内でのジェネリクスの型決定} ジェネリクスに実際の型を入れる際に、DataGearの定義時に決定するケースがある。 ソースコード\ref{src:PhilsImplG}は、Phils InterfaceのImplであるPhilsImplの型定義ファイルである。 Implが持つ変数として、AtomicTにintを具体的な型として与えた型を定義している。 \lstinputlisting[label=src:PhilsImplG, caption=Implファイル内でのジェネリクスの型の決定]{src/PhilsImpl.h} \subsection{CodeGear定義内でのジェネリクスの型決定} 型定義をヘッダファイルでする以外の方法として、 CodeGearで定義するケースもある。 ソースコード\ref{src:useGene}に示す例では、 InterfaceであるAtomicTはint型が型決定される。 実装のコンストラクタであるcreateAtomicTImplも、int型で型決定されている。 \lstinputlisting[label=src:useGene, caption=GenericsのCodeGear内での型決定]{src/useGene.cbc} \subsection{ジェネリクスの型決定手法} 確認したように、ジェネリクスは型変数の定義と型に具体的な値をいれる2種類の使い方がある。 ジェネリクスを導入する場合、これらのどちらの文脈で使われているかをPerlトランスパイラ側で判定する必要がある。 これにはInterfaceのパーサーで取得できる、型変数のリスト情報を使用する。 ジェネリクスは\texttt{<>}記号の中に型変数、もしくは具体的な型をいれる。 解析しているファイルに登場する\texttt{<>}の中の文字列が、型変数として登録されていた場合は、そのソースコードは具体的な型をいれていない型定義ファイルであると捉える。 逆に型変数情報になかった場合は具体的な型が決定された状況である。 この場合は、どのDataGearであるかと、どの型に決定されたかを記憶する。 \subsection{ジェネリクスの型生成} ジェネリクスに代入された具体的な型が決定した後は、ジェネリクスの型をそれぞれの具体的な型に合わせて変形させる必要がある。 これはGearsOSはCbC上に実装されているが、CbCは型変数や、ジェネリクスの\texttt{<>}の様な構文をサポートしていない為である。 つまり、ジェネリクスで拡張された構文を、等価なCbCのソースコードに書き換える必要がある。 GearsOSでは次のアルゴリズムで型を変形する。 \begin{itemize} \item Interfaceの名前の末尾に\texttt{\_具体的な型}を与える \begin{itemize} \item AtomicT Interfaceにintを与えた場合、 AtomicT\_int \end{itemize} \item Implの名前の末尾に\texttt{\_具体的な型}を与える \begin{itemize} \item AtomicTImpl の場合intを与えた場合、 AtomicT\_intImpl\_int \end{itemize} \end{itemize} この操作はすべてのCbCファイルについて操作する必要があるため、generate\_context.pl内で行われる。 すべてのCbCファイル、ヘッダファイルについて調査を行い、型変数と具体的な型の組を作製する。 CodeGearとDataGearによって、型決定後のオペレーションが異なる。 DataGearの場合はcontext.hに書き込むunion Data型で計算で使うすべての型が決定する。 そのため、union Data型を作製するDataGearの集合から、型変数を持つ型を削除し、代わりに具体的な型をあてはめた型をいれる。 これによって、ジェネリクスで確定した型がcontext.hに書き込まれるようになる。 CodeGearの場合は、変換した.cファイルを再度開き、使用されているジェネリクスの記述を置換する。 置換自体は上記のアルゴリズムに沿って行われ、型変数の宣言に使われる\texttt{<>}記法は削除される。 型変数を持つInterface/Implのコンストラクタが含まれるファイルは、対応する具体的な型の分コードを生成する。 関数名も置換されるため衝突は発生しない。 実際に変換したコードをソースコード\ref{src:Phils2}に示す。 この例では、AtomicTにintを具体的な型として与えていた為、AtomicT\_int型が生成されている。 \lstinputlisting[label=src:Phils2, caption=AtomicTの型をジェネリクスによってAtomicT\_intに置換した例]{src/Phils2.cbc} \section{generate\_stub.plのデバッグ機能の追加}\label{sec:generateStubDebug} 変換されたGearsOSのコードが意図しない結果になっていた場合、 generate\_stub.plのデバッグをする必要がある。 PerlスクリプトであるのでPerlのデバッガを使えばデバッグは可能である。 しかし、変換するGearsOSの行数もあり、さらにgenerate\_stub.pl自体の複雑度から、バグを生じている場所の検討をつけるのが難しい。 generate\_stub.plは巨大な正規表現パターンマッチで構成されたスクリプトであるので、 CbCのコードのどの行を呼んでいる時に、 Perlスクリプトのどの行にマッチしたかが重要となる。 本来マッチしてほしい正規表現パターンにマッチしていないケースは、どの行にマッチしたかのログが解れば一発で確認することができる。 また、怪しい正規表現パターンの行にPerlデバッガでbreak pointを張ってデバッグすることも可能である。 この機能をgenerate\_stub.plの起動時オプションの形で実装した。 \texttt{--debug}オプションをつけると、デバッグ表示が行われる。(ソースコード\ref{src:debugGenStubSh}、 \ref{src:debugGenStub}) \lstinputlisting[label=src:debugGenStubSh, caption=generate\_stub.plのデバッグモードでの起動]{src/debugGenerateStubLog.sh} \lstinputlisting[label=src:debugGenStub, caption=マッチしたPerlスクリプトの行番号と、CbCコードの対応表示]{src/debugGenerateStubLog.txt} Perlスクリプトでどの行にマッチしたかの情報は、 Perlの特殊変数\texttt{\_\_LINE\_\_}を利用した。 この特殊変数は、特殊変数を呼び出したPerlの行番号が取得できるメタAPIである。 \lstinputlisting[label=src:debugPrint, caption=debug\_print]{src/debugPrint.pl} \lstinputlisting[label=src:callDebugPrint, caption=debug\_printの呼び出し]{src/callDebugPrint.pl} \section{GearsOS初期化コードの自動生成} \label{sec:gmain} GearsOSでは、TaskManagerやWorker、Contextの初期化を起動時に行わなければならない。 GearsOSの例題では、これらの初期化関数や、初期化に関連するCodeGearはほぼ共通であった。 新しい例題を実装する際に、これらの初期化ルーチンを毎回コピー\&ペーストし、実際に記述したい箇所を変更するという手法がとられていた。 この初期化ルーチンも一種のメタ的な操作であるために、ユーザーから分離させたい。 そこで自動で初期化ルーチンを作製する、特殊なCodeGearの定義である\texttt{gmain}を定義した。 gmainで定義したmainファイルをソースコード\ref{src:gmainExample}に示す。 gmainを使用するCbCファイルは、 他のファイルと変わらずに、 Interfaceを使う場合は\texttt{\#interface}構文で呼び出す。 通常と異なるのは、 GearsOSを停止させるCodeGearとしてshutdownがAPIとして定義されている。 gotoで継続するCodeGearの引数として、このshutdownを渡すと、最後にGearsOSの終了ルーチンに継続するようになる。 これをmain.cbcとして定義する。 \lstinputlisting[label=src:gmainExample, caption=gmainを使ったMainCodeGear定義]{src/ppGmain.cbc} generate\_stub.plで、main.cbcを変換した後をソースコード\ref{src:afterGmain}に示す。 \lstinputlisting[label=src:afterGmain, caption=gmain定義の変換後のCbC Code]{src/afterGmain.cbc} 変換した結果では、 コマンドライン引数でGearsOSのWorkerの数などを指定できる関数initや、 TaskManagerの初期化を行うinitDataGears、GearsOSを終了するCodeGearであるshutdownなどが生成される。 gmainのスコープの中に記述したものは、createTaskの中に移動される。 createTaskでは、GearsOSの起動に必要なTaskManagerなどの引数が受け渡されるようになっている。 これによってGearsOSの例題を実装する際に、考慮しなければいけない煩雑な設定を緩和することが可能となった。