DRYな備忘録

Don't Repeat Yourself.

cgoの中で#ifdefのようなプラットフォーム分岐をしてはいけない:clang: error: unsupported option '-fopenmp'

このエントリはGoのカレンダー | Advent Calendar 2022 - Qiitaの13日目です。多種多様なGoに関する記事を見ることができて、とても刺激になってます。ありがとう、クリスマス。ありがとう、アドベントカレンダー。当方はというと、最近ふつうにハマった問題の共有をしたいなと思います。

問題

cgoを使ったパッケージを開発中に、プラットフォームに依存したCフラグなどを渡す必要があり、cgoの中で #ifdef __APPLE__#ifdef __FreeBSD__ を書くものの、正しく分岐されず間違ったプラットフォームに間違ったビルドフラグが渡されるような挙動を観測した。

先に結論

  1. #cgoディレクティブはgoがCをコンパイルするよりも前に評価される。
  2. したがって、Cのプリプロセッサ#if#ifdefなど)により#cgoを分岐しようとしても、前もって評価されてしまうため、#cgoディレクティブに書かれたものが当該プラットフォームにとって正しくなければ掲題のようなエラーを吐く。
  3. これを避けるために、Goのファイル構成の段階で//go:buildないしxxxx_unix.goなどのファイル名suffixを使ってcgoへ渡す前に分岐しなければならない
  4. 雑にChatGPTにエラーメッセージを投げるの、勢いづけにめちゃくちゃ良かった!

追記





以下、ログなので読まなくていいです。メリークリスマス!★



ログ

  1. FreeBSDで起きている問題の認識と解決の方向性
    1. 長いことメンテしている自作のGoライブラリがあるんですが、こちらで、1年以上前からFreeBSDのCIがコケていることが観測されていた。
    2. CIの失敗ログを見ると、"ld-elf.so.1: /usr/local/lib/libtesseract.so.5: Undefined symbol "__kmpc_global_thread_num" というエラーメッセージとともにコケていることが分かった。
    3. __kmpc_global_thread_numとは、どうやら、OpenMPという並列計算を可能にするAPIを提供するライブラリに定義される定数のもよう。
    4. libtesseract.soOpenMPによって定義される定数を見に行こうとして、リンクされておらず困っているのだと推察。
    5. どうやら、LDFLAGSなどに-fopenmpを与えれば、解決しそうなので、Goのimport "C"をしているファイルの冒頭にフラグを追加すればよさそう。
  2. openmp問題の解決により新たに生じた問題
    1. 前述の通り、Goのimport "C"をしているファイルの冒頭に-fopenmpのフラグを追加した。⇒Fix test for vagrant · otiai10/gosseract@9be0318 · GitHub
    2. GitHub-hosted runners にpreinstallされているVirtualBox+Vagrantを利用したFreeBSDのテストは、通るようになった。
    3. 一方で、macOS上のテストは掲題のエラーとともにコケるようになった。
    4. Goのimport "C"をしているclient.goでは、#if __FreeBSD__ >= 10 を用い#cgoに与えるフラグを出し分けしているはずなのに、ナゼ...?
  3. Goファイルにおける#cgoディレクティブの振る舞いに関する気付き
    1. #ifディレクティブの書き方が良くない可能性に仮説を持ち、#ifdef __APPLE__#ifdef __FreeBSD__ に書き直してテストするが、同じ結果を得る。
    2. __APPLE____FreeBSD__などのマクロ定数が定義されていない可能性に仮説を持ち、gcc -E -dM - </dev/nullprintfデバグで確認するが、残念ながら__APPLE__は存在し__FreeBSD__は存在しない、正しい結果を得るものの、テストの結果は同じである。
    3. #define __APPLE__ 1#undef __FreeBSD__を冒頭に挿入しても、#ifdef __FreeBSD__のブロックへ突入するような挙動で、同じ結果を得る。
    4. この時点で、C/C++プリプロセッサが評価されるタイミングと、#cgoが評価されるタイミングが異なる可能性に気づく。Special Thanks id:moriyoshi
  4. Goファイルのレイヤでプラットフォーム依存のコードを分割し、解決を得る
    1. FreeBSDに与えたいLDFLAGSの定義は、preprocess_freebsd.goというファイルに定義し、それ以外に与えたいLDFLAGSはpreprocess_x.goというファイルに定義することで、解決した。
    2. 本件における最終成果物 ⇒ Separate Go files to control platform-specific Cgo flags · otiai10/gosseract@060b832 · GitHub

実際に上記のプロジェクトでコケていたCIのログ

上部は実際のコードだが、下部はデバグのために仕込んだコード。
①のセクションでは#ifdefで分岐がされていない?ように見える

参考資料

リファレンス

参考

雑感

自作Slack-botにChatGPTを組み込んだので話が早い...!

当ブログとは関係無いんですが、GoからOpenAI ChatGPTのAPIを利用できるためのAPIクライアントを作ったので、スターやissueくれるとうれしいです!

github.com

DRYな備忘録として

HTMLのformタグのaction属性にqueryパラメータを設置したが、サーバ側で取得できていない

問題

クライアント(HTML)側

<form method="GET" action="/form/submit?nickname=otiai10">
  <input type="submit" value="送信" />
</form>

サーバ(Go)側

func HandleFormSubmit(w http.ResponseWriter, req *http.Request) {
    fmt.Fprintf("nickname: %s", req.URL.Query().Get("nickname"))
}

ブラウザで見える出力

nickname: 

つまり来てない。

検証方法

  • ブラウザのURL欄に直で、
https://自サーバ/form/submit?nickname=otiai10

と入力して訪問。するとブラウザでは

nickname: otiai10

と表示される

原因

formタグをGETメソッドで使用するとき、action属性に付加する ?nickname=otiai10 などのqueryパラメータは送信時に無視され、inputタグを見てparameterが再構築される。

stackoverflow.com

解決

  • action属性の中でqueryを使うな
  • inputタグ(e.g. type="hidden")を使え
-  <form method="GET" action="/form/submit?name=otiai10">
+  <form method="GET" action="/form/submit">
+   <input type="hidden" name="nickname" value="otiai10" />
    <input type="submit" value="送信" />
  </form>

雑感

  • input[type=hidden]なんて久しぶりに書いた。
  • 上記は検証用ゆえに、form[method=GET]を使っていたり、任意の入力値をサニタイズせずにブラウザに表示したりしており、商用コードとしては問題あるので、ご注意ください。
  • 世の中はAIだのブロックチェーンだの言っているが、俺はまだ世界の片隅でHTMLを書いてる...

DRYな備忘録として

GitHub Actions からの deno deploy が失敗する: Error: The deployment failed: Relative import path "$fresh/server.ts" not prefixed with / or ./ or ../

問題

以下のようにデプロイのGitHub Actionsを設定したが、掲題のエラーを得る。

    steps:
      - name: Clone repository
        uses: actions/checkout@v2
      - name: Upload to Deno Deploy
        uses: denoland/deployctl@v1
        with:
          project: "fresh-youtube"
          entrypoint: "./main.ts"

github.com

原因

解決

    steps:
      - name: Clone repository
        uses: actions/checkout@v2
      - name: Upload to Deno Deploy
        uses: denoland/deployctl@v1
        with:
          project: "fresh-youtube"
          entrypoint: "./main.ts"
+.        import-map: "./import_map.json"

github.com

雑感

  • deno、ノーストレスでTypeScriptが書けるのがよい。
  • あと、dependenciesのimportが直接的かつ明示的なのがよい。
  • 仕事でコード書かないからこそ、いつも触ってない技術をもっと積極的に触っていかなくてはならないと感じた。
  • ということで fresh framework で使えるめちゃくちゃ良い感じのコンポーネント作ったのでぜひStarしてあげてください。

github.com

DRYな備忘録として




datastore: invalid entity type

問題

cloud.google.com/go/datastore

datastore package - cloud.google.com/go/datastore - pkg.go.dev

を使ってDatastore  |  Google CloudにデータをPutしていて、掲題のエラーを得た。

ev := models.Event{/* なんらかの値 */}
if _, err := tx.Put(key, ev); err != nil {
    return err
}
datastore: invalid entity type

調査

まずエラーメッセージが貧弱なので、定義を知りたい。

Search · datastore: invalid entity type · GitHub

あった google-cloud-go/datastore.go at 1063c601a4c4a99217b45be0b25caa460e7157a1 · googleapis/google-cloud-go · GitHub

f:id:otiai10:20210905185048p:plain

あらためて、順当にPutを掘っていく。

f:id:otiai10:20210905190549p:plain

結論

Getのみならず、Put系であっても、Structの値はPointerである必要がある。

ev := models.Event{/* なんらかの値 */}
- if _, err := tx.Put(key, ev); err != nil {
+ if _, err := tx.Put(key, &ev); err != nil {
    return err
}

雑感

  • たしかにエラーメッセージとして十分だとは思うが、自作パッケージをつくるなら、もうちょっと丁寧に返したい
  • 最近、WETもDRYもぜんぜん書いてなくてさみしい

DRYな備忘録として

ameshコマンド & amesh Slack bot の最近の話

このエントリは Go 2 Advent Calendar 2020 - Qiita の8日目です。

背景

  • もう2年前になりますが、ソフトウェアを主業としない業界に転職しました
  • 最近はストレス解消にコードを書いています。仕事ではいっさいコード書かない
  • ameshコマンド、amesh-botは便利なので今でもガンガン開発しています
  • そろそろ艦これウィジェットのサーバサイドをHerokuからGCPに引っ越したい

ameshつくったの、もう5年前じゃん。

tl;dr

  • GAE/Go 1.12+の環境で動くようにリファクタリングしました
  • 「予報」コマンドをつくりました
  • Slack AppとしてDistributeしました(Submitはまだ)
  • それにともなってFirestore使うようにしました
  • 今まで0.07円だったんですけど、公開したら614円になりました

github.com

CLIとWebアプリケーションを含むプロジェクトの構成について

ameshプロジェクトは、ameshコマンドと、amesh-botという2つのユースケースを提供しています。さらに、他のプロジェクトからもimportできるpackageとしてのインターフェースもあります。

当初は、以下のようなファイル構成で作成していました。

.
├── bot
├── amesh
│   └── main.go
└── lib

このコマンドラインツールをインストールしようとすると go get github.com/otiai10/amesh/amesh というように、amesh/ameshをgetする必要があります。これは、一時期けっこう見たタイプだと思いますたとえばgithub.com/robfig/revel/revelとか。

revel、いっぺん死んだかと思ったら、ひさしぶりに見に行ったらふつうに1.0.0が最近リリースされててびっくりした。 この情報を見つけただけでもこのアドカレ書いた意味あったかもしれん。

いつぞやはお世話になりました、revel。

あとはservice/owner/foo/cmd/fooみたいなパターンもよく見ますが、ameshに関してはかっこよさを優先してgo get github.com/otiai10/ameshとしたくて、ルートにmain.goを配置しました。

.
├── bot # Slackのwebhookを受けるサーバの定義
├── cli # コマンドラインアプリケーションの定義
├── lib # 外部プロジェクトがimportできるもの
└── main.go # コマンドのエントリポイント

Slack AppのDistributeにあたって、Firestoreの利用

データを保存する必要があるため、GAE/GoからFirestoreへの接続をしました。が、めっちゃかんたんだったのであんまり特筆すべきことはないです。

これ見てやりました。

pkg.go.dev

ちなみに

気温も見れます

マイナンバー通知カードが見つかったら、GitHub Sponsorを始めると同時に「Add to Slack」ボタンを公開したいと思います。

マイナンバー通知カードどこ〜

誰か俺のマイナンバー通知カードを見つけてくれ

DRYな備忘録として

スターティングGo言語

スターティングGo言語

Go言語による並行処理

Go言語による並行処理

改訂2版 基礎からわかる Go言語

改訂2版 基礎からわかる Go言語