2021年 6月 2日

OpenCL の OSS 実装 pocl を調べる その 2 - 独自アクセラレータのテンプレート実装を眺める

目次: OpenCL を調べる - まとめリンク

CPU でも GPU でもないデバイスで OpenCL を動かすとしたらどうしたら良いでしょうか?答えとしては、その 1 で紹介したとおり、CL_DEVICE_TYPE_ACCELERATOR を実装すれば良いです。が、イチから作るのはとっても大変です。

pocl のテンプレート実装

素晴らしいことに pocl にはテンプレートらしき実装が pocl/lib/CL/devices/accel に用意されています。やりたいこととは微妙に違うことが後々わかりますが、イチから作るよりははるかにマシです。このテンプレートを改造しましょう。

テンプレートの名前は accel でいかにもアクセラレータに見えますが、デバイスタイプは CL_DEVICE_TYPE_ACCELERATOR ではなく CL_DEVICE_TYPE_CUSTOM です。CUSTOM は「コンパイルが可能なデバイス」ではなく、ビルトインカーネルのみを実行するデバイスです。ユーザー定義のカーネルを実行することは考えられていません。

ユーザー定義カーネルが実行できることが独自アクセラレータの売りですから、何とかして CUSTOM ではなく ACCELERATOR になるように実装を改造する必要があります。これはなんとも先が長そうです……。

編集者: すずき(更新: 2021年 6月 10日 16:45)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 3日

OpenCL の OSS 実装 pocl を調べる その 3 - デバイス数の取得処理

目次: OpenCL を調べる - まとめリンク

独自アクセラレータのテンプレート実装 pocl/lib/CL/devices/accel はデバイスタイプが CUSTOM になっているのが最大の難関ですが、その他にも色々問題があります。

最初に遭遇する問題はデバイス数を取得する処理のエラー処理が間違っていることです。現状のコードだとちょっと特殊な環境変数を渡さないと動きません。

pocl テンプレート実装 accel のデバイス数取得処理

// 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() が正しくデバイス数を返すような実装を追加する必要があるでしょうが、この場は環境変数で切り抜けます。

編集者: すずき(更新: 2021年 6月 10日 16:54)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 4日

OpenCL の OSS 実装 pocl を調べる その 4 - デバイスのパラメータを渡す環境変数

目次: 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 も環境変数に頼らない実装に変えていく必要がありますが、今はそのままにしておきます。

編集者: すずき(更新: 2021年 6月 10日 17:14)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 5日

今更 HiFive Unleashed の JTAG に気づいた

昔買った HiFive Unleashed というボード、JTAG はちょっと特殊なコネクタが必要だと思っていて接続を諦めていました。ところが今日、ファンを掃除したあと動作確認をした際に、USB Serial と一緒に USB JTAG も用意されていることに気づきました。

HiFive Unleashed はディスコンでもう手に入りませんし、後継機種の HiFive Unmatched も買った今となっては、かなり今更感ありますが……。

ちょっと試したところ、簡単に OpenOCD が繋がり SMP モードにすると 5コア(rv64imac な E51 と rv64gc な U54 x 4)が見えました。

HiFive Unleashed 用の OpenOCD コンフィグ

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 から転記しておくことにした。加筆。

編集者: すずき(更新: 2021年 6月 9日 01:50)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 6日

Zephyr on HiFive Unleashed

目次: Zephyr を調べる - まとめリンク

JTAG が繋がった(2021年 6月 5日の日記参照)記念に Zephyr を HiFive Unleashed で動かしてみました。使うハードウェアは UART だけ、コードも JTAG で ITM にロードして動作させるだけなら楽勝だろと思いきや、全然 UART から文字が出力されず 1日掛かってしまいました。

Zephyr on HiFive Unleashed の UART ログ
*** 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 から転記しておくことにした。加筆。

編集者: すずき(更新: 2021年 6月 9日 01:56)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 7日

GTK のウインドウサイズ変更がシビアすぎる

Twitter で GUI の話をしている人を見かけて思い出した話です。私は GNU/Debian Linux の GUI を LightDM + GTK にして使っています。

GUI にこだわりはなく一時期デフォルトだった LightDM を使い続けているだけです。デフォルトになっただけあって、良くできていると思うし特に不満はないです……が、1点だけ言わせてもらえば、ウインドウサイズ変更の判定が厳しすぎませんか?


LightDM + GTK のウインドウサイズ変更判定の位置

特に左辺、上辺、右辺が 1px しか反応してくれないので、マウス操作が非常にシビアです。手が震えてきます。

操作が良くない

Twitter で上記の話をしていたところ、Alt + 右マウスボタンでウインドウサイズを変えられるよ、と教えてもらいました。今度からそうします。1px に合わせるのは手が疲れる……。

編集者: すずき(更新: 2021年 6月 25日 12:55)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 8日

OpenCL の OSS 実装 pocl を調べる その 5 - デバイスの初期化

目次: OpenCL を調べる - まとめリンク

引き続き、独自アクセラレータのテンプレート実装 pocl/lib/CL/devices/accel の細かな問題を調べます。次の問題は OpenCL の初期化です。clGetPlatformIDs() から初期化関数 pocl_accel_init() に辿り着いたところで abort() が呼ばれクラッシュします。

/dev/mem を開く際のエラーログ
  |   GENERAL |  accel: accelerator at 0x1000 with 0 builtin kernels
Could not open /dev/mem

テンプレート実装の意図としては /dev/mem を open() してメモリマップされたハードウェアのレジスタを読み書きしたいようです。今回は実際のハードウェア相手ではないので、レジスタの読み書きではなく /dev/mem の代わりにバイナリファイルを開いてもらうように書き換えます。

/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); のように固定値を書いておくと次に進みます。今は実際のデバイスが相手ではないので、とりあえず先に進めて後で考えましょう。

編集者: すずき(更新: 2021年 6月 10日 17:35)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 10日

HiFive Unmatched 用の SSD 購入

目次: 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 で十分でしょう。きっと。

編集者: すずき(更新: 2021年 6月 28日 15:21)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 17日

Raspberry Pi 3 のオーディオ その 8 - シミュレーションと実測値の差(解決編)

目次: Raspberry Pi - まとめリンク

Raspberry Pi 3 の Audio Out の最後の謎がわかりました。

  • PWM の Duty 比 100%を維持したときに減衰する速度が異なります。

その 6(2021年 5月 12日の日記参照)にて Raspberry Pi 3 の回路図が間違っているのでは?と疑っていましたが、違いました。ケーブルに入っている抵抗のせいでした。

抵抗入りケーブル

今まで測定に使用していたオーディオケーブルにはプラグ内に抵抗が入っています。そもそもなんでこんなの買ったんだろ……?プラグの見た目からはわかりませんので、テスターで各端子間の抵抗を計測した結果は下記のとおりです。

ミニ Lミニ Rミニ GRCA LRCA GRCA RRCA G
ミニ L --- 294 14746.7k 14746.7k 147
ミニ R --- 14747.0k 14746.4k 147
ミニ G ---47.0k 047.0k 0
RCA L ---47.0k94.0k47.0k
RCA G ---47.0k 0
RCA R ---47.0k
RCA G ---

測定結果から想定される回路図です。左がミニジャック側、右が RCA プラグ側です。


想定される抵抗入りケーブルの回路図

再度シミュレーション

この結果を踏まえてシミュレーションすると実測値とほぼ一致しました。


Audio Out 回路のシミュレーション結果(125Hz 矩形波を入力に設定)ケーブルの抵抗を考慮


Audio Out 回路の実測値(黄色 Audio Out、水色 PWM 信号 125Hz 矩形波)

気づいてみれば何とも初歩的なミスでしたが、ケーブルは 0Ωと思い込んで見落としました。他人(RasPi の回路図)を疑う前に自分を疑えという良い教訓ですね〜。

編集者: すずき(更新: 2021年 6月 19日 01:09)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする




2021年 6月 20日

読書一生分

書籍通販の honto がこんなキャンペーンをやっています。


honto 読書一生分プレゼントキャンペーン

このキャンペーン画像を見たときの率直な感想としては、どんな人間を想定したら、読書一生分がたった 93万円に収まるのか?でした。マンガしか読んでない自分でさえ 100万じゃ 10年も持ちません。

1世帯あたり読書にいくら使う?

思い込みで文句を言うのは良くないなと思って、統計データを見ました。総務省統計局 - 読書に関する支出(2018年)によると、1世帯、読書の支出が年間 10,628円(電子書籍含まず)です。電子書籍を含む値段で考えたとしても、さほど変わりません。電子書籍を最も購入している 30代(世帯主の年齢)でも 1,736円で、読書支出は 12,000円程度だからです。

世帯の読書支出 10,628円 x 日本人の平均寿命 84年 = 892,752円となり、honto のキャンペーン金額と大体同じくらいになります。あながち間違った数値でもなかった、ということですね。

1人あたり読書にいくら使う?

先程のデータを見ていて何が驚いたって、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円は厳しいです。世間の生活が想像できません……。

編集者: すずき(更新: 2021年 6月 25日 12:33)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 21日

mplayer, mpv でイコライザーを使う

いつもわからなくなるのでメモしておきます。mplayer にてイコライザーを使う方法です。最近は mpv と呼ぶんですかね?

コマンドは mpv を使いますが、実はイコライザー機能は ffmpeg の一部である libavfilter.so に頼っています(avfilter のドキュメントへのリンク)。この構造は一見しただけではわかりにくく、ヘルプを探すときに非常に難儀しました。設定方法も独特でいつも書き方がわからなくなります。

イコライザーは superequalizer という名前です(superequalizer のドキュメントへのリンク)。18バンド指定できます。各バンドがどの周波数帯に対応するかはドキュメントを見てください。

mpv で avfilter の superequalizer を設定する例
$ 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 のバージョン
$ 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 になっていて若干気になりますけど、特に害なさそうだから良いのかな……。

編集者: すずき(更新: 2021年 6月 29日 11:37)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 23日

プログラムから LLVM を実行する その 1 - 準備編

目次: LLVM を調べる - まとめリンク

LLVM や Clang は実行する方法が 2つあります。1つ目はみなさまお馴染みのコマンドラインから実行する方法で、2つ目はプログラムから Clang のライブラリを通して実行する方法です。

特に後者のプログラムから実行する方法は GCC では真似できませんから、LLVM ならではの機能と言えるでしょう。ただ、ちょっとインタフェースが不安定というか、バージョンによってちょいちょい変わって動かなくなるようで、そこは玉に瑕ですね。

LLVM ビルド&インストール

Clang/LLVM をプログラムから実行するにはいくつか準備が必要です。大まかに分けると LLVM のビルド&インストールと、ヘッダおよびライブラリパスの指定です。

ビルドは以前もチャレンジしました(2019年 3月 26日の日記参照)。基本的には cmake と make(または ninja)です。それは変わりませんが、いくつか追加したいオプションがあるので再掲します。

LLVM のビルドオプション
$ 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_INSTALL_PREFIX
インストール先を指定します。システムに既にインストールされている LLVM を破壊しないよう、ビルドディレクトリの隣の _install ディレクトリにインストールする指定です。
LLVM_TARGETS_TO_BUILD
以前(2019年 3月 27日の日記参照)も使いましたが、特定ターゲットのみをビルドするオプションで、ビルド時間の短縮に繋がります。2つ以上指定する場合はセミコロンで繋ぎましょう。例では x86 と RISC-V 向けにしていますが、お好きなアーキテクチャを足してください。
LLVM_BUILD_LLVM_DYLIB
全てのライブラリを 1つのライブラリ libLLVM.so に集約するオプションです。興味があればこのオプションの ON/OFF により後述する llvm-config の出力がどう変化するか確認すると面白いかもしれません。
LLVM_ENABLE_PROJECTS
LLVM は LLVM 以外にも多彩なツールを持っています。どのツールをビルドするか選択するオプションです。全ては必要ないですが少なくとも clang は後で必要になります。例では全部入りにしています。

CMake の実行が成功したら、ninja install を呼びましょう。インストールまで進むはずです。

Makefile の作成

ヘッダインクルードパスの指定、ライブラリパスの指定のために Makefile を書きます。パスの細かい値について心配する必要はありません。llvm-config というツールが用意されており、ほぼ全て自動的に用意してくれます。Makefile の一例を示すと、

テスト用の 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 にリンクするような方法が見当たらなかったからです。良い方法をご存知の方は教えていただけると嬉しいです。

これで準備完了です。続きは次回に。

編集者: すずき(更新: 2021年 6月 28日 19:51)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



2021年 6月 24日

プログラムから LLVM を実行する その 2 - プリプロセス編

目次: LLVM を調べる - まとめリンク

準備が終わりましたら Clang/LLVM をプログラムから呼びましょう。

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 のインクルードパスを調べる
$ 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++ などに渡すとコンパイル可能なので、おそらく変な出力にはなっていないでしょう。

編集者: すずき(更新: 2021年 6月 28日 21:00)

コメント一覧

  • コメントはありません。
open/close この記事にコメントする



こんてんつ

open/close wiki
open/close Java API

過去の日記

open/close 2002年
open/close 2003年
open/close 2004年
open/close 2005年
open/close 2006年
open/close 2007年
open/close 2008年
open/close 2009年
open/close 2010年
open/close 2011年
open/close 2012年
open/close 2013年
open/close 2014年
open/close 2015年
open/close 2016年
open/close 2017年
open/close 2018年
open/close 2019年
open/close 2020年
open/close 2021年
open/close 過去日記について

その他の情報

open/close アクセス統計
open/close サーバ一覧
open/close サイトの情報