目次: OpenCL
CPUでもGPUでもないデバイスでOpenCLを動かすとしたらどうしたら良いでしょうか?答えとしては、その1で紹介したとおり、CL_DEVICE_TYPE_ACCELERATORを実装すれば良いです。が、イチから作るのはとっても大変です。
素晴らしいことにpoclにはテンプレートらしき実装がpocl/lib/CL/devices/accelに用意されています。やりたいこととは微妙に違うことが後々わかりますが、イチから作るよりははるかにマシです。このテンプレートを改造しましょう。
テンプレートの名前はaccelでいかにもアクセラレータに見えますが、デバイスタイプはCL_DEVICE_TYPE_ACCELERATORではなくCL_DEVICE_TYPE_CUSTOMです。CUSTOMは「コンパイルが可能なデバイス」ではなく、ビルトインカーネルのみを実行するデバイスです。ユーザー定義のカーネルを実行することは考えられていません。
ユーザー定義カーネルが実行できることが独自アクセラレータの売りですから、何とかしてCUSTOMではなくACCELERATORになるように実装を改造する必要があります。これはなんとも先が長そうです……。
目次: OpenCL
独自アクセラレータのテンプレート実装pocl/lib/CL/devices/accelはデバイスタイプがCUSTOMになっているのが最大の難関ですが、その他にも色々問題があります。
最初に遭遇する問題はデバイス数を取得する処理のエラー処理が間違っていることです。現状のコードだとちょっと特殊な環境変数を渡さないと動きません。
// pocl/lib/CL/devices/devices.c
static unsigned device_count[POCL_NUM_DEVICE_TYPES];
...
cl_int
pocl_init_devices ()
{
...
/* Init operations */
for (i = 0; i < POCL_NUM_DEVICE_TYPES; ++i)
{
...
/* Probe and add the result to the number of probed devices */
assert(pocl_device_ops[i].probe);
device_count[i] = pocl_device_ops[i].probe(&pocl_device_ops[i]); //★デバイス数を取得する★
pocl_num_devices += device_count[i];
}
...
dev_index = 0;
/* Init infos for each probed devices */
for (i = 0; i < POCL_NUM_DEVICE_TYPES; ++i)
{
if (pocl_devices_init_ops[i] == NULL)
continue;
str_toupper (dev_name, pocl_device_ops[i].device_name);
assert(pocl_device_ops[i].init);
for (j = 0; j < device_count[i]; ++j) //★デバイス数42億と誤解したまま処理しようとしてクラッシュする★
{
// pocl/lib/CL/devices/accel/accel.cc
unsigned int pocl_accel_probe(struct pocl_device_ops *ops) {
//★POCL_DEVICESという環境変数が見つからないとき、-1というエラー値を返す★
//★本来エラー値である -1だが、デバイス数として解釈され42億になってしまう★
int env_count = pocl_device_get_env_count(ops->device_name);
return env_count;
}
// pocl/lib/CL/devices/devices.c
/**
* Get the number of specified devices from environment
*/
int pocl_device_get_env_count(const char *dev_type)
{
const char *dev_env = getenv(POCL_DEVICES_ENV);
char *ptr, *saveptr = NULL, *tofree, *token;
unsigned int dev_count = 0;
if (dev_env == NULL)
{
return -1; //★ここにくる★
}
ptr = tofree = strdup(dev_env);
while ((token = strtok_r (ptr, " ", &saveptr)) != NULL)
{
if(strcmp(token, dev_type) == 0)
dev_count++;
ptr = NULL;
}
POCL_MEM_FREE(tofree);
return dev_count;
}
このような実装になっておりaccelのデバイス数が42億(!)と解釈されてしまい、42億回デバイスを列挙しようとしてクラッシュします。バグのような気がしますけど、サンプル実装ですのであまり文句を言っても仕方ありません。
環境変数POCL_DEVICES="pthread -1 CUDA -1 accel 1" のようにデバイス数を明示的に渡せば回避可能です。最終的にはpocl_accel_probe() が正しくデバイス数を返すような実装を追加する必要があるでしょうが、この場は環境変数で切り抜けます。
目次: OpenCL
引き続き、独自アクセラレータのテンプレート実装pocl/lib/CL/devices/accelの細かな問題を調べます。デバイス数の取得の問題を回避すると、次はデバイスのパラメータを渡す問題に遭遇します。
// pocl/lib/CL/devices/accel/accel.cc
void pocl_accel_init_device_ops(struct pocl_device_ops *ops) {
ops->device_name = "accel"; //★★デバイス名はaccel
ops->init = pocl_accel_init;
...
// pocl/lib/CL/devices/devices.c
cl_int
pocl_init_devices ()
{
...
dev_index = 0;
/* Init infos for each probed devices */
for (i = 0; i < POCL_NUM_DEVICE_TYPES; ++i)
{
if (pocl_devices_init_ops[i] == NULL)
continue;
str_toupper (dev_name, pocl_device_ops[i].device_name); //★★dev_nameはデバイス名を大文字に変換したACCELになる
assert(pocl_device_ops[i].init);
for (j = 0; j < device_count[i]; ++j)
{
...
/* Check if there are device-specific parameters set in the
POCL_DEVICEn_PARAMETERS env. */
POCL_GOTO_ERROR_ON (
(snprintf (env_name, 1024, "POCL_%s%d_PARAMETERS", dev_name, j) //★★環境変数名を生成する箇所
< 0),
CL_OUT_OF_HOST_MEMORY, "Unable to generate the env string.");
errcode = pocl_devices[dev_index].ops->init (
j, &pocl_devices[dev_index], getenv (env_name));
...
実装ではpocl_accel_init() にて環境変数の値をパースしてデバイスのパラメータを取得します。環境変数名はデバイス番号によって変化しますが、0番目のデバイスであればPOCL_ACCEL0_PARAMETERSという名前になります。環境変数名は上記にあるとおりpocl_init_devices() で決めています。
困ったことに環境変数が見つからないとabort() してしまうので、環境変数には最低でも何か1つ数値を渡す必要があります。なお1つ目の値はレジスタ領域のベースアドレスだと解釈されるようです。
他の実装(pthreadとcuda)は環境変数を使わないので、同様の問題は存在しません。最終的にはaccelも環境変数に頼らない実装に変えていく必要がありますが、今はそのままにしておきます。
目次: RISC-V
昔買ったHiFive Unleashedというボード、JTAGはちょっと特殊なコネクタが必要だと思っていて接続を諦めていました。ところが今日、ファンを掃除したあと動作確認をした際に、USB Serialと一緒にUSB JTAGも用意されていることに気づきました。
HiFive Unleashedはディスコンでもう手に入りませんし、後継機種のHiFive Unmatchedも買った今となっては、かなり今更感ありますが……。
ちょっと試したところ、簡単にOpenOCDが繋がりSMPモードにすると5コア(rv64imacなE51とrv64gcなU54 x 4)が見えました。
adapter speed 10000
adapter driver ftdi
ftdi_device_desc "Dual RS232-HS"
ftdi_vid_pid 0x0403 0x6010
ftdi_layout_init 0x0008 0x001b
ftdi_layout_signal nSRST -oe 0x0020 -data 0x0020
set _CHIPNAME riscv
jtag newtap $_CHIPNAME cpu -irlen 5 -expected-id 0x20000913
set _TARGETNAME $_CHIPNAME.cpu
target create $_TARGETNAME.0 riscv -chain-position $_TARGETNAME -rtos hwthread
target create $_TARGETNAME.1 riscv -chain-position $_TARGETNAME -coreid 1
target create $_TARGETNAME.2 riscv -chain-position $_TARGETNAME -coreid 2
target create $_TARGETNAME.3 riscv -chain-position $_TARGETNAME -coreid 3
target create $_TARGETNAME.4 riscv -chain-position $_TARGETNAME -coreid 4
target smp $_TARGETNAME.0 $_TARGETNAME.1 $_TARGETNAME.2 $_TARGETNAME.3 $_TARGETNAME.4
init
halt
SiFiveのSoCはFU540も後継のFU740も命令セットの違うコアを混載するのが好きですね。混載は良いとしてせめて同じ命令セットにしてほしかったです。単純なSMPしか持ってないOSだと制御できないじゃん……。
メモ: 技術系の話はFacebookから転記しておくことにした。加筆。
目次: Zephyr
JTAGが繋がった(2021年6月5日の日記参照)記念にZephyrをHiFive Unleashedで動かしてみました。使うハードウェアはUARTだけ、コードもJTAGでITMにロードして動作させるだけなら楽勝だろと思いきや、全然UARTから文字が出力されず1日掛かってしまいました。
*** Booting Zephyr OS build zephyr-v2.6.0-39-g2bf63134e8f0 *** thread_a: Hello World from cpu 0 on hifive_unleashed! thread_b: Hello World from cpu 0 on hifive_unleashed! thread_a: Hello World from cpu 0 on hifive_unleashed! thread_b: Hello World from cpu 0 on hifive_unleashed! thread_a: Hello World from cpu 0 on hifive_unleashed! thread_b: Hello World from cpu 0 on hifive_unleashed!
原因はコアのPLL設定が間違っていて、コアクロックから分周して作るUARTのボーレートもおかしな周波数になっていたからです。
FU540のリセット直後は外部クロック源(33.33MHz)をそのまま使って動きます。起動後PLLを1GHz(33.33 / 1 * 120 / 4 = 999.9MHz)に設定し、コアクロックをPLL側に切り替えなければなりません。
PLLの設定はFSBLという2段目のブートローダーが行うので、通常はOSが気にする必要はありません。しかしRTOSは大抵ブートローダーを経由しませんから、ブートローダーに隠れた設定も拾ってきて実装しないと動かないことがあります。
ブートローダーがあって当たり前のリッチ系SoCでRTOSを動かそうとすると、大抵この「暗黙のうちに設定されている何か」が抜け落ちて動かなかったりおかしくなったり、何かと面倒が起きます。
幸いなことにSiFiveのブートローダーはコードが公開されており、完全ブラックボックスのSoCと比べれば、難易度は低い部類です。ありがたいですね。
メモ: 技術系の話はFacebookから転記しておくことにした。加筆。
目次: Linux
TwitterでGUIの話をしている人を見かけて思い出した話です。私はGNU/Debian LinuxのGUIをLightDM + GTKにして使っています。
GUIにこだわりはなく一時期デフォルトだったLightDMを使い続けているだけです。デフォルトになっただけあって、良くできていると思うし特に不満はないです……が、1点だけ言わせてもらえば、ウインドウサイズ変更の判定が厳しすぎませんか?
特に左辺、上辺、右辺が1pxしか反応してくれないので、マウス操作が非常にシビアです。手が震えてきます。
Twitterで上記の話をしていたところ、Alt + 右マウスボタンでウインドウサイズを変えられるよ、と教えてもらいました。今度からそうします。1pxに合わせるのは手が疲れる……。
目次: OpenCL
引き続き、独自アクセラレータのテンプレート実装pocl/lib/CL/devices/accelの細かな問題を調べます。次の問題はOpenCLの初期化です。clGetPlatformIDs() から初期化関数pocl_accel_init() に辿り着いたところでabort() が呼ばれクラッシュします。
| GENERAL | accel: accelerator at 0x1000 with 0 builtin kernels Could not open /dev/mem
テンプレート実装の意図としては /dev/memをopen() してメモリマップされたハードウェアのレジスタを読み書きしたいようです。今回は実際のハードウェア相手ではないので、レジスタの読み書きではなく /dev/memの代わりにバイナリファイルを開いてもらうように書き換えます。
// pocl/lib/CL/devices/accel/accel.cc
cl_int pocl_accel_init(unsigned j, cl_device_id dev, const char *parameters) {
...
POCL_MSG_PRINT_INFO("accel: accelerator at 0x%zx with %zu builtin kernels\n",
D->BaseAddress, D->SupportedKernels.size());
int mem_fd = open("/dev/mem", O_RDWR | O_SYNC);
if (mem_fd == -1) {
POCL_ABORT("Could not open /dev/mem\n"); //★★このabortでクラッシュ
}
...
ファイル名を書き換えて突破するとデバイスが持っているメモリのサイズを取得し、メモリマップしようとする部分で怒られます。
| GENERAL | accel: accelerator at 0x1000 with 0 builtin kernels a.out: ../lib/CL/devices/accel/accel.cc:196: void MMAPRegion::Map(size_t, size_t, int): Assertion `Data != MAP_FAILED && "MMAPRegion mapping failed"' failed.
サイズを取得している箇所は下記のとおりです。
// pocl/lib/CL/devices/accel/accel.cc
cl_int pocl_accel_init(unsigned j, cl_device_id dev, const char *parameters) {
...
uint32_t ctrl_size = D->ControlMemory.Read32(ACCEL_INFO_CTRL_SIZE);
uint32_t imem_size = D->ControlMemory.Read32(ACCEL_INFO_IMEM_SIZE);
uint32_t dmem_size = D->ControlMemory.Read32(ACCEL_INFO_DMEM_SIZE);
uint32_t pmem_size = D->ControlMemory.Read32(ACCEL_INFO_PMEM_SIZE);
uint32_t max_region =
std::max(std::max(ctrl_size, imem_size), std::max(dmem_size, pmem_size));
D->InstructionMemory.Map(D->BaseAddress + max_region, imem_size, mem_fd);
...
バイナリファイルを書き換えて何か適当な値が読めるようにしてやりすごすか、面倒ならばD->ControlMemory.Write32(ACCEL_INFO_CTRL_SIZE, 0x2000); のように固定値を書いておくと次に進みます。今は実際のデバイスが相手ではないので、とりあえず先に進めて後で考えましょう。
目次: RISC-V
買い物メモです。先日(2021年5月28日の日記参照)SiFive HiFive Unmatchedを購入しました。このボードはmicroSDからブートしますが、追加のストレージとしてNVMe SSDが装着できます。
Western DigitalのWDS100T2B0C-ECを購入しました。Amazonで13,000円くらいでした。容量1TB、規格M.2 2280、接続NVMeです。コストパフォーマンス重視のWD Blueシリーズです。
WD BlueシリーズはWD Blackシリーズと比較すると速度で見劣りするものの、そもそもHiFive UnmatchedのCPUはそれほど速くないですしWD Blueで十分でしょう。きっと。
目次: Raspberry Pi
Raspberry Pi 3のAudio Outの最後の謎がわかりました。
その6(2021年5月12日の日記参照)にてRaspberry Pi 3の回路図が間違っているのでは?と疑っていましたが、違いました。ケーブルに入っている抵抗のせいでした。
今まで測定に使用していたオーディオケーブルにはプラグ内に抵抗が入っています。そもそもなんでこんなの買ったんだろ……?プラグの見た目からはわかりませんので、テスターで各端子間の抵抗を計測した結果は下記のとおりです。
ミニL | ミニR | ミニG | RCA L | RCA G | RCA R | RCA G | |
---|---|---|---|---|---|---|---|
ミニL | --- | 294 | 147 | 46.7k | 147 | 46.7k | 147 |
ミニR | --- | 147 | 47.0k | 147 | 46.4k | 147 | |
ミニG | --- | 47.0k | 0 | 47.0k | 0 | ||
RCA L | --- | 47.0k | 94.0k | 47.0k | |||
RCA G | --- | 47.0k | 0 | ||||
RCA R | --- | 47.0k | |||||
RCA G | --- |
測定結果から想定される回路図です。左がミニジャック側、右がRCAプラグ側です。
この結果を踏まえてシミュレーションすると実測値とほぼ一致しました。
Audio Out回路のシミュレーション結果(125Hz矩形波を入力に設定)ケーブルの抵抗を考慮
Audio Out回路の実測値(黄色Audio Out、水色PWM信号125Hz矩形波)
気づいてみれば何とも初歩的なミスでしたが、ケーブルは0Ωと思い込んで見落としました。他人(RasPiの回路図)を疑う前に自分を疑えという良い教訓ですね〜。
目次: RISC-V
関係の深いまとめリンク。
SiFive社ボードの話、CoreMarkの話のまとめ。
その他の話のまとめ。
目次: マンガ紹介
書籍通販のhontoがこんなキャンペーンをやっています。
このキャンペーン画像を見たときの率直な感想としては、どんな人間を想定したら、読書一生分がたった93万円に収まるのか?でした。マンガしか読んでない自分でさえ100万じゃ10年も持ちません。
思い込みで文句を言うのは良くないなと思って、統計データを見ました。総務省統計局 - 読書に関する支出(2018年)によると、1世帯、読書の支出が年間10,628円(電子書籍含まず)です。電子書籍を含む値段で考えたとしても、さほど変わりません。電子書籍を最も購入している30代(世帯主の年齢)でも1,736円で、読書支出は12,000円程度だからです。
世帯の読書支出10,628円x日本人の平均寿命84年 = 892,752円となり、hontoのキャンペーン金額と大体同じくらいになります。あながち間違った数値でもなかった、ということですね。
先程のデータを見ていて何が驚いたって、1世帯で1年間たった1万円しか本を買わないことです。この時点で少ないなと思うんですけど……。1世帯には複数人が生活していますので、1人あたりの支出も計算してみます。
世帯の平均人数はe-Statで調べることができます。平均世帯人員、年次別(平成27年国民生活基礎調査 世帯票 報告書掲載 年次推移 表番号7)を見ると、2015年で1世帯平均2.49人です。
世帯あたり読書の支出は1年10,628円(書籍7,478円、雑誌3,150円)割ることの、日本の平均世帯人数2.49人(減少傾向)ですから、1人あたり1年で4,268円(書籍3,003円、雑誌1,265円)です。さらに少なくなりました。
例えば、週刊少年ジャンプ(定価270円x 50冊 = 13,500円)をもれなく買うだけで3倍以上の支出になります。普段全く本は買わない、くらいじゃないと1年4,268円は厳しいです。世間の生活が想像できません……。
目次: ALSA
いつもわからなくなるのでメモしておきます。mplayerにてイコライザーを使う方法です。最近はmpvと呼ぶんですかね?
コマンドはmpvを使いますが、実はイコライザー機能はffmpegの一部であるlibavfilter.soに頼っています(avfilterのドキュメントへのリンク)。この構造は一見しただけではわかりにくく、ヘルプを探すときに非常に難儀しました。設定方法も独特でいつも書き方がわからなくなります。
イコライザーはsuperequalizerという名前です(superequalizerのドキュメントへのリンク)。18バンド指定できます。各バンドがどの周波数帯に対応するかはドキュメントを見てください。
$ mpv --no-video --af=volume=0.8,superequalizer=1.2:1.5:1.5:1.2:1.2:1:1:1:1:1:1:1:1:1:1:1:1:1 a.mp4 Video --vid=1 (*) (h264 480x360 6.000fps) (+) Audio --aid=1 (*) (aac 2ch 44100Hz) AO: [pulse] 44100Hz stereo 2ch float A: 00:00:01 / 00:04:40 (0%) Cache: 278s/9MB
上記の例では、映像を出さない(--no-video)、音割れ防止の為にvolumeで8割くらいに音を下げる、superequalizerの18バンドを全て設定しています。superequalizer=1b=1.2:2b=1.5のようにすると特定のバンドだけ設定変更できます。便利な方を使ってください。
$ mpv --version mpv 0.32.0 Copyright © 2000-2020 mpv/MPlayer/mplayer2 projects built on UNKNOWN ffmpeg library versions: libavutil 56.51.100 libavcodec 58.91.100 libavformat 58.45.100 libswscale 5.7.100 libavfilter 7.85.100 libswresample 3.7.100 ffmpeg version: 4.3.2-0+deb11u2
動作確認に使ったmpvのバージョンも記録しておきます。なぜならffmpegやmpvはたまにインタフェースが激変するので、将来的に同じ方法が通用しなくなる可能性が高いからです。使用しているディストリビューションはDebian Testingです、今はDebian 11相当みたいですね。
なぜかbuilt on UNKNOWNになっていて若干気になりますけど、特に害なさそうだから良いのかな……。
目次: LLVM
LLVMやClangは実行する方法が2つあります。1つ目はみなさまお馴染みのコマンドラインから実行する方法で、2つ目はプログラムからClangのライブラリを通して実行する方法です。
特に後者のプログラムから実行する方法はGCCでは真似できませんから、LLVMならではの機能と言えるでしょう。ただ、ちょっとインタフェースが不安定というか、バージョンによってちょいちょい変わって動かなくなるようで、そこは玉に瑕ですね。
Clang/LLVMをプログラムから実行するにはいくつか準備が必要です。大まかに分けるとLLVMのビルド&インストールと、ヘッダおよびライブラリパスの指定です。
ビルドは以前もチャレンジしました(2019年3月26日の日記参照)。基本的にはcmakeとmake(またはninja)です。それは変わりませんが、いくつか追加したいオプションがあるので再掲します。
$ cmake \ -G Ninja \ ../llvm \ -DCMAKE_INSTALL_PREFIX=`pwd`/../_install \ -DCMAKE_C_COMPILER=clang \ -DCMAKE_CXX_COMPILER=clang++ \ -DCMAKE_BUILD_TYPE=RelWithDebInfo \ -DBUILD_SHARED_LIBS=ON \ -DLLVM_ENABLE_ASSERTIONS=ON \ -DLLVM_TARGETS_TO_BUILD="X86;RISCV;NVPTX" \ -DLLVM_USE_LINKER=lld \ -DLLVM_BUILD_LLVM_DYLIB=OFF \ -DLLVM_LINK_LLVM_DYLIB=OFF \ -DLLVM_ENABLE_PROJECTS="clang;clang-tools-extra;compiler-rt;debuginfo-tests;libc;libclc;libcxx;libcxxabi;libunwind;lld;lldb"
ざっくり意図を説明すると下記のとおりです。オプションの正確な意味についてはLLVM公式ドキュメント(Build LLVM with CMake - LLVM 12 documentation 参照)を見てください。
CMakeの実行が成功したら、ninja installを呼びましょう。インストールまで進むはずです。
ヘッダインクルードパスの指定、ライブラリパスの指定のためにMakefileを書きます。パスの細かい値について心配する必要はありません。llvm-configというツールが用意されており、ほぼ全て自動的に用意してくれます。Makefileの一例を示すと、
LLVM_CONFIG_PATH = /path/to/llvm-project/_install/bin
LLVM_CONFIG = $(LLVM_CONFIG_PATH)/llvm-config --link-shared
CPPFLAGS = $(shell $(LLVM_CONFIG) --cppflags)
CFLAGS = $(shell $(LLVM_CONFIG) --cflags) -g
CXXFLAGS = $(shell $(LLVM_CONFIG) --cxxflags) -g
LDFLAGS = $(shell $(LLVM_CONFIG) --ldflags)
LIBS = -lclang-cpp $(LLVM_CONFIG) --libs --system-libs engine)
clang_test: main.o
$(CXX) $(CXXFLAGS) $(LDFLAGS) -o $(APP) $< $(LIBS)
基本的にはllvm-config --xxxflagsとするとオプションに指定すべき文字列が出力されますから、素直に各種FLAGSに渡すだけです。もちろん何かオプションを追加するのも自由です。例では -gを足しています。
LIBSのところがちょっと格好悪いのは、llvm-configでlibclang-cppにリンクするような方法が見当たらなかったからです。良い方法をご存知の方は教えていただけると嬉しいです。
これで準備完了です。続きは次回に。
目次: LLVM
準備が終わりましたらClang/LLVMをプログラムから呼びましょう。
int main(int argc, char *argv[])
{
bool success;
clang::CompilerInstance CI;
clang::CompilerInvocation &build = CI.getInvocation();
// 引数の配列を作成する
std::vector<const char*> vec_args;
vec_args.push_back("-I/usr/include/c++/10");
vec_args.push_back("-I/usr/include/x86_64-linux-gnu/c++/10");
vec_args.push_back("-I/usr/include/c++/10/backward");
vec_args.push_back("-I/usr/lib/llvm-11/lib/clang/11.0.1/include");
vec_args.push_back("-I/usr/include/x86_64-linux-gnu");
vec_args.push_back("-I/usr/include");
vec_args.push_back("-I/path/to/llvm-project/_install/include");
// エラーメッセージを出力するために使われるクラス
llvm::IntrusiveRefCntPtr<clang::DiagnosticIDs> diagID = new clang::DiagnosticIDs();
llvm::IntrusiveRefCntPtr<clang::DiagnosticOptions> diagOpts = new clang::DiagnosticOptions();
clang::TextDiagnosticBuffer *diagBuffer = new clang::TextDiagnosticBuffer();
clang::DiagnosticsEngine diags(diagID, diagOpts, diagBuffer);
CI.createDiagnostics(diagBuffer, false);
// コンパイラ呼び出し用のインスタンスを作成する
llvm::ArrayRef<const char*> ref_args(vec_args.data(), vec_args.data() + vec_args.size());
success = clang::CompilerInvocation::CreateFromArgs(build, ref_args, diags);
// コンパイラフロントエンドのオプション設定
// 入力ソースコード: test.cpp
// 出力ソースコード: test.preproc.cpp
const char *source_file = "test.cpp";
const char *preproc_file = "test.preproc.cpp";
clang::FrontendOptions &fe = build.getFrontendOpts();
clang::InputKind ik = clang::InputKind(clang::Language::CXX);
clang::FrontendInputFile fif = clang::FrontendInputFile(source_file, ik);
fe.Inputs.clear();
fe.Inputs.push_back(fif);
fe.OutputFile.assign(preproc_file);
// プリプロセスのオプション設定
// 言語: C++11
clang::PreprocessorOptions &po = build.getPreprocessorOpts();
clang::LangOptions *la = build.getLangOpts();
llvm::Triple triple = llvm::Triple();
build.setLangDefaults(*la, ik, triple, po.Includes, clang::LangStandard::lang_cxx11);
// 下記のようにオプションの一部だけ変えることもできる
//la->CPlusPlus = true;
//la->CPlusPlus11 = true;
// プリプロセスのオプション
// コメント、定義済みマクロなどは出力しない
clang::PreprocessorOutputOptions &poo = build.getPreprocessorOutputOpts();
poo.ShowCPP = true;
poo.ShowComments = false;
poo.ShowLineMarkers = false;
poo.ShowMacros = false;
poo.ShowMacroComments = false;
poo.RewriteIncludes = false;
// プリプロセス実行(失敗したらエラーログを出力する)
clang::PrintPreprocessedAction Preprocess;
success = CI.ExecuteAction(Preprocess);
if (!success) {
get_build_log(diagBuffer, (CI.hasSourceManager()) ? &CI.getSourceManager() : nullptr);
}
}
残念ながらこの呼び出し方が正解とは断言できません。探した限りではどう呼び出すべきか書かれたドキュメントも見当たりませんでした。上記の例はpoclを参考にしており、大きな間違いはないはずですが……。何かやらかしていたら教えていただけると嬉しいです。
動作確認はLLVM 12で行いました。他のバージョンだとAPIの引数などが変わっているので、ビルドすら通らないと思います。LLVMの困ったところですね……。
上記のサンプルでは引数で -Iオプションを使ってインクルードパスを指定します。インクルードパスは頑張ってヘッダファイルがある場所を調べても良いですが、おそらく同じ名前のヘッダが複数の場所にあって混乱すると思いますから、PCで動作しているClang++ から拝借するのが簡単です。
$ clang++ test.cpp -v Debian clang version 11.0.1-2 Target: x86_64-pc-linux-gnu Thread model: posix InstalledDir: /usr/bin Found candidate GCC installation: /usr/bin/../lib/gcc/x86_64-linux-gnu/10 ... #include "..." search starts here: #include <...> search starts here: /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10 /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/x86_64-linux-gnu/c++/10 /usr/bin/../lib/gcc/x86_64-linux-gnu/10/../../../../include/c++/10/backward /usr/lib/llvm-11/lib/clang/11.0.1/include /usr/include/x86_64-linux-gnu /usr/include End of search list. ...
いろいろなメッセージが出力されますが、インクルードパスは "search starts here:" の辺りに書かれています。出力は特に捻りはなくディレクトリ名そのものですので、頭に -Iを足せばオプションの出来上がりです。
プリプロセスを実行します。テスト用のプログラムは下記のとおりです。
#include <iostream>
int main(int argc, char *argv[])
{
// This is comment
std::cout << "Hello, world!!" << std::endl;
}
$ make $ ./clang_test
ファイル名などは完全に決め打ちのため引数は必要ありません。実行に成功するとプリプロセス後のソースコードtest.preproc.cppが作成されているはずです。
namespace std
{
typedef long unsigned int size_t;
typedef long int ptrdiff_t;
typedef decltype(nullptr) nullptr_t;
}
...
static ios_base::Init __ioinit;
}
int main(int argc, char *argv[])
{
std::cout << "Hello, world!!" << std::endl;
}
私の環境で実行したところ27,000行くらいあるファイルになりました。たった1つしかヘッダをincludeしてないのに凄まじい行数に展開されます。コメントは消えていますが、オプションを変更すれば残すこともできます。PreprocessorOutputOptionsのShowComments = trueにすると残ります。
$ g++ test.preproc.cpp $ ./a.out Hello, world!!
プリプロセス後のソースコードをg++ などに渡すとコンパイル可能なので、おそらく変な出力にはなっていないでしょう。