マクロレス型安全printf

もうみんなconstexprに飽きてしまったのか、ほとんど文献がないのでメモ程度に。 私が考えたわけではなく、 constexpr で テンプレートメタプログラミング - TXT.TXT に書いてあったことを試してみたというだけです。 紹介する実装は、コンセプトの確認にとどまっており、完全な実装というわけではありません。 また、静的型付けにこだわらなければ、boost.printfは変な型が来た時に実行時エラーにしてくれるらしいですが、せっかくC++でやっているのにと感じてしまうかもしれません。実際、静的型付けを行うiostreamでは実行時型エラーは起きないため、利便性と引き換えに安全性が失われてしまっているという感じがあります。

Cプリプロセッサマクロは基本的にC++erから嫌われているので、それを使わないで純粋なC++の文法のみで書けるのは一定の成果なのですが、特に読みやすくなるわけではありません。

本題と関係ない話

Twitterとかマストドンとかってすごく検索性が低いなぁと感じます。特に、空リプ(空中リプライ、話題の引用がない会話)みたいなものは運よく検索で見つかってもほとんど情報を得られないです。 マストドンに至っては、この前に流行った時に立った草の根インスタンスはほとんど消滅してしまっています(おそらく、AWSなどを使うと最初の一年間は無料でできるので、それが終わるタイミングで消滅したのでしょう)。

そういうところに技術的な話をうずめてしまうのが悲しいので、ブログみたいな少しは検索性のあるところに書き留めておこうと思って書いています。

ltmpcの時にいろいろなブログ記事にお世話になったから……。

実際のコード

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

ユーザー側では、

print( []{return"%d\n";}, a );

のように書けます。型が正しくないコードを書くと、TMPでちゃんとコンパイルエラーになってくれるはずです(ただし、実装されているのは%d%c%p%%くらいなので、それ以外の書式指定文字列だと書式指定文字列として認識されず、「引数が多いよ!」というコンパイルエラーになると思います)。

少し不思議な点

この方法の肝心な点は、「ラムダ式でくるめば値(項)を型に変換できる」ということです。書式指定文字列は全てconst char*の型になってしまい、内容によって型が変わるということはありません。 しかし型安全なprintfを実現するためには、書式指定文字列ごとに別の型になってくれないと型検査ではじけません。逆に書式指定文字列ごとに別の型になっていれば、TMPでどうにかできるはずです。

ここでラムダ式の、「それぞれ固有の型になる」という特性が活用できます。単にconst char*を返すだけのラムダ式であっても、書式指定文字列が違えば別の型になってくれて目的が達せられます(もっとも、書式指定文字列が全く同一でも別の場所に書くだけで異なる型になってしまいますが……)。

ここで不思議なのは、

template<class Str>
auto ToFormatString( Str str ) -> decltype( ToFormatString_emptyCheck<str()[0]>{}( str ) );

のようなコードで、仮引数strメンバ関数呼び出しをした結果をToFormatString_emptyCheck構造体の非型テンプレート引数にしても普通にコンパイルできる点です。 実際のところ、Str型のどんなオブジェクトからも同じ結果が返ってくるのでこれは問題にはなりませんが、strにアクセスした時点ではread of non-constexpr variable 'str' is not allowed in a constant expressionというコンパイルエラーにならないのはちょっと不思議です。 ちなみに、ラムダ式や自作関数オブジェクトにして、内部状態を持たせ、operator()内部でその内部状態に依存する分岐を行わせようとすると確かにread of non-constexpr variable 'str' is not allowed in a constant expressionというコンパイルエラーが発生します。

つまり、this->operator()()を行った時点ではthisに触ったことにはならず、this->xみたいなメンバ変数にアクセスした時点でread of non-constexpr variable 'str' is not allowed in a constant expressionコンパイルエラーになるのですね。

知識なしでMastodonを改造する

先週の記事でも言ったけれど、私はこの分野の知識はほぼゼロなので、余り役には立たないかも。

Mastodonオープンソースです。オープンソースのよいところはいろいろありますが、そのうちの一つに「気に入らなければ勝手に改造できる」というのがあります。

Mastodonのタイムラインは(Twitterと違って)時系列順に並んでいますが、(Twitterと同じく)相対時間表示になっています。これは気に入らないので改造していきます。

相対時間表示の代わりに絶対時間表示にする

Mastodonの規模はそれほど大きいわけではありませんが、ソースコードを一から読んでいくというのはそれなりに厳しそうです。 そもそも、この分野の知識はほとんどないので、その勉強から始めるとなると気が遠くなります。 そこで、いろいろ眺めてみて、どこが変更に必要そうなところかを探っていきます。

htmlのソースコードを見ていくと、datetime="2018-08-17T22:42:51.543Z"のようなものが入っていることが分かります。それから、5分前みたいなのは普通に書かれていることが分かります。少し待っていると6分前に変わったので、きっとJavascriptでこの辺が操作されているということが推測できます。

そのJavascriptファイルを探し当てたいところですが、どうやら難読化(というより短縮化)がかかっているようで、元のファイルを探す必要がありそうです。

datetimegrepをかけてみれば、きっとその辺のソースコードが見つかるはずです。……とやってみると、かなりの数が引っかかってしまいました。ダメもとでtime datetimeで検索してみると……うまくいきました。app/javascript/mastodon/components/relative_timestamp.jsといういかにもそれらしいファイルが見つかります。実際、中を見てみるとまさにこれが求めるものだということがすぐにわかります。

改造すべき場所はわかりましたが、どうやれば絶対時間を表示できるかの問題を解決していかないといけません。そもそも、文字列をどのように扱うのかがいまいちわからない……。

幸いヒントがあります。七日より前の場合、相対時間表示ではなく絶対時間表示にするようなコードが書かれているので、そこをまねすればよさそうです。

{ ...obj, tag: val }という構文が見慣れないですが、使われ方から見て、「objの要素はそのまま、tag: valを追加する」くらいの意味合いで使われていそうです。よくわかりませんがまねして書いてみます。

(どうやらECMAScript 2018で追加された新機能で、スプレッド構文というらしい。参考文献: Mastodon フロントエンド改造入門 #thebossblog こういう構文に関する情報を無から探し当てるのは初学者には無理があるし、やはり一から学ぶしかないのでしょうか……。)

とはいっても相対時間表示はそれなりに役立つので併記することにしましょう。できあがったコミットがこちら→

Add absolute time · lpha-z/mastodon@b6e2638 · GitHub

(8月18日)のように無駄にかっこがついてしまう問題がありますが、大きな問題ではないし、文字列の扱い方がよくわからないのでしょうがない。今度勉強して直しておきましょう。

コンパイル

ブラウザに流れ込んできているJavascriptコンパイル済みのものになっているので、再コンパイルが必要です。インスタンスを立ち上げた時のウィザードをもう一度やり直せばOKです。そんな遠回りをしなくても、RAILS_ENV=production bundle exec rails assets:precompileを行えばよいです。これをどう見つけるかという話ですが、ウィザードに書いてあります。また、デーモンを再起動(stopしてからstart)する必要があります。

相対時間表示を詳細化する

先々週くらいに言っていた、「1時間前、って書かれても幅が広すぎてわかりづらい!」という文句を改善していきたいと思います。

ソースファイルは、先と同じrelative_timestamp.jsです。そもそも、このソースファイルの中には、5分前という表示ができるだけの情報がないように思われます。「分前」という文字列がどこにもないです。 きっと多言語対応のために別のファイルに定義があるのだと推測します。実際、分前grepすると、app/javascript/mastodon/locales/ja.jsonが引っかかります。どうやらさっきの推測は正しかったようです。

あとは簡単で、適当に修正すればうまくいくはずです。できあがったコミットがこちら↓

Detail relative time (Japanese only) · lpha-z/mastodon@42be44b · GitHub

実はこれはダメ

これでうまくいったと思っていましたが、相対時間表示が1時間0分で止まってしまいました。実は相対時間部分の更新を行うJavascriptは常に動いているわけではなくて、定期的に活動しているようです。で、その更新頻度が1時間に一回になっているため、1時間0分で止まってしまっているようです。UnitDelayって最初何のことかと思っていましたが更新頻度のことだったようです。というか、後から見てみれば_scheduleNextUpdateの中で使用されているし、明らかに更新頻度の定義です(この関数は意味がよくわからず最初読み飛ばしていました……)。

というわけで修正したコミットがこちら↓

Increase frequency of updating relative time · lpha-z/mastodon@a317ef9 · GitHub

reblog(ブースト)の時刻を表示する

これも重要な情報だと私は思うのですが、どうやら表示されていないようです。これも追加したいところですが、今までの手法だけでは太刀打ちできなさそうです。 つまり、今までの二つの変更は情報の表示方法の問題だったのに対し、今回行いたい変更は、新たに情報を持ってくる必要があります。 とはいえ、まずはどこを改造するかを探し当てるところからです。

まず、htmlを眺めているとstatus__wrapperがキーワードになっているようなので、これでgrepします。運よくapp/javascript/mastodon/components/status.jsの一件だけが見つかったのでこれでまず間違いないでしょう。

しかし、肝心のreblogされた時刻はこのソースファイル中では使う予定がないため、適当な変数に代入されている様子がありません。おそらくデータとしてはあるはずですが(そうでないと時系列順にならばないはずです)、オブジェクトのどこに格納されているかが分かりません。 型なし言語はこういう時に困ります……。

このソースファイルのthis.props(なのかな?)に渡されているオブジェクトを構築している部分を探し当てる必要があります。 あるいは、ブラウザでデバッグコンソールにダンプしてもよさそうです。

私の場合は、上記手段のやり方がよくわからなかったため、このソースコードをじっくり眺めました。その結果、

  • status.get('reblog')はreblogだったときに限って、そのreblogしたトゥートが入っている
  • status.get('account')は、
    • 普通のトゥートの時は、そのトゥートをしたアカウントの情報が入っている
    • reblogの時は、reblogしたアカウントの情報が入っている(reblogしたトゥートをしたアカウントの情報ではない)

ということが読み取れます(92, 93行目で変数の上書きを行っているところ)。

また、トゥートを行った時刻は、<RelativeTimestamp timestamp={status.get('created_at')} />という形でRelativeTimestampに渡されていることも読み取れます。 つまり、reblog元のトゥートの作成時間は、status.get('reblog').get('created_at')に入っています。

時刻を格納する変数は一つしかなくて、そこにはトゥート時刻が収まっているためreblog時刻はオブジェクトの中には入っていない、といったパターンも考えられましたが、そうなっている可能性は低そうであることが分かりました。

そこで、status.get('created_at')にreblog時刻が入っていると信じてやってみると……。うまくいきました。

できあがったコミットはこちら↓

Show reblog time (Japanese only) · lpha-z/mastodon@696fafd · GitHub

ふぁぼられた時間を表示する

Twitterにもあるし、これくらいの表示はしたいところです。

app/javascript/mastodon/locales/ja.jsonから逆算して、notification.favouriteなどが含まれているソースファイルを調べてみると、app/javascript/mastodon/features/notifications/components/notification.jsが該当することが分かります。

まず先ほどと同じように<RelativeTimestamp timestamp={notification.get('created_at')} />などとやってみると、コンパイルでエラーになります。 これは当然で、import RelativeTimestamp from '../../../components/relative_timestamp';がないといけないということはすぐにわかります。

この修正を行えばコンパイル自体は通るのですが、ブラウザ側でエラーになります。やはりcreated_at要素がないようです。

やはりこのソースファイルにわたってくるオブジェクトを作成している、上位のソースファイルを探す必要がありそうです。

さんざんいろんなソースファイルを探し回った挙句、app/javascript/mastodon/reducers/notifications.jsを発見しました。

加える変更はたった一行ですが探すのが非常に大変でした……。

できあがったコミットはこちら↓

Show notification time (Japanese only) · lpha-z/mastodon@a74d261 · GitHub

おわりに

さっぱり知識がない状態で、いかに改造すべきポイントを探し当てて改造していくのかという過程を紹介してみました。

みんなもMastodonインスタンスを立てて改造してみましょう。

知識ゼロからマストドンインスタンスを立てた

自分用にマストドンインスタンスを立てました。→ https://radon.lpha-z.net

いろいろとさびしいので、遊びに来てくれるとうれしいです。 Twitterの方を動かさなくなるというわけではないです。

さて、マストドンインスタンスを立てたい誰かの助けになればと思って、やったことをメモしておきます。 注意点として、私はネットワーク方面の知識を全く持っていないので、結構まずいことをやっているかもしれないです。 そういう点の指摘があれば、コメント欄に書いていただけると助かります(大歓迎です)。

基本的には知識ゼロでできるはずですが、Linux系のコマンドを全く叩いたことがないという場合は少し難しいかもしれません(もちろん、手順通りやればいいのですが、やってることの意味が全くわからないと、間違った時に解決方法がわからないかもしれません)。

要約

  • サーバーは、AWSを使う(一年間は無料でできます)
  • ロードバランサーとかデータベース用サーバーとかは使わないで、EC2のインスタンス一個でやる(私がよくわからないのと、小規模ならその方がお金もかからないし……)
  • 自分用のドメインが必要です(一年間で1000円とか2000円とかそこらです)

手順

AWSに入門する

まずはAWSのアカウントを作りましょう。クレジットカードの登録が必要ですが、無料の範囲内でやっていきます。

次に、以下の記事の3.までを読んでAWSに関する知識を得ながら構築を進めます。注意としては、4.以降にかかれている情報は古い(前にマストドンが流行った2017年4月ごろのものにもとづいている)ので、そこは行いません。あと、ドメインをRoute53で入手したり、Amazon Certificate ManagerでSSL証明書を入手したり*1、Hosted Zoneの設定をしたりは今回はやりません。

AWSでMastodonインスタンスを作るまで。自分まとめ

上の記事ではAMIの選択とインスタンスタイプが説明されていません。立てるスタンスは、Ubuntu Server 18.04LTSが良いようなのですが、無料の範囲内にそういったのがないので、.NET Core 2.1 with Ubuntu Server 18.04 - Version 1.0 (ami-6ccaa781)を選びました。本当にこれでいいのかな?インスタンスタイプは、無料になっているt2.microを選びました。 上の記事と異なる点として、セキュリティグループの設定では、HTTPとHTTPSも許可します。HTTPはなくてもよいような気がします。

ドメインを入手し、DNSを設定する

自分用のドメイン(なんとか.comとか、そういうやつです。私のだとlpha-z.net)を手に入れます。私はお名前.comで取得しました。特にこれを推奨しているわけではありませんが、以下の説明はお名前.com特有の説明になります。DNSについての最低限の知識があれば、他のところで取得しても、以下の手順を適当に自分の取得したところ用の手順に読み替えて行えるはずです。そういったサービスがないのなら、Route53でやればよいような気もします。0.5ドルかかるらしいです。

ドメインを入手した後、ログインします。DNSのタブを開きます。DNSレコード指定で、自分のマストドンインスタンスのホスト名(私のだとradon.lpha-z.net。.lpha-z.netはすでに入っているのでradonとだけ入力)を入力します。TYPEはAとして、TTLはなんでもよいのでとりあえずデフォルトのままにしておきます。VALUEは、EC2のインスタンスに割り当てられたIPv4 パブリックIPを入力します。

ところで、ここで入力するホスト名は、マストドンインスタンスを立ち上げてから変えようと思うと面倒なことになるので、慎重に決めた方が良いです。マストドンアカウントは、それ込みになっているからです。

マストドンインスタンスを立ち上げる

ネット上にある記事は、ほとんど2017年4月ごろのものなので(大事なことなので二回言いました)、参考にすると痛い目にあいます。 公式のドキュメント documentation/Production-guide.md at master · tootsuite/documentation · GitHub を眺めながら行いましょう。

以下、手順を詳しく説明します。上のドキュメントに、私の理解での補足説明を加えていますが、怪しい部分が多いと思います。Linuxの知識が少しでもあり、英語が全く読めないというわけではなければ、上のドキュメントを見れば十分だと思われます。

ちなみに、大慌てでやっても一時間くらいかかります。

スワップファイルの設定をする

最後の方の「コンパイル」のところでメモリが足りなくなると困るので、念のためスワップファイルの設定をしておきます。t2.microのメモリ量だと本当にぎりぎりのようです。

sudo fallocate -l 4G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab

依存しているソフトウェアをインストールする

この部分はrootユーザーでやってくださいと書いてあります。これは、ubuntuであれば、sudoをつけて実行してくださいという風に読み替えれば十分です。以下の説明では必要なところにsudoを加えて説明していますから、その通りに入力すれば大丈夫なはずです。

node.jsを入れる

node.jsとは、サーバーサイドJavaScript環境だそうです(Wikipedia)。 まず、

sudo apt -y install curl

curlというアプリケーションをインストールします。おそらく、これは更新不要と出るはずです。 次に、

curl -sL https://deb.nodesource.com/setup_8.x | sudo bash -

と入力してnode.jsをインストールします。sudoの位置はここです。これは、このURLから入手したスクリプトを実行するというコマンドになっています。 何が送り付けられてくるのかはよくわかりませんが、とにかくよしなにやってくれるようです。

yarnを入れる

yarnはJavascript用のパッケージマネージャーだそうです。以下のコマンドを入力し手インストールします。

curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt update

入力ミスに気を付けます。入力ミスをしてもOKなどと表示されるのですが、以降の手順でエラーになってしまうという罠が存在します(私は引っかかりました)。 こちらは得体のしれないスクリプトを実行するのではなく、aptというパッケージマネージャーを通して行っているようです。

その他の依存関係を入れる

そのほかの、aptコマンドで入れられるものをたくさん入れます。

sudo apt -y install imagemagick ffmpeg libpq-dev libxml2-dev libxslt1-dev file git-core g++ libprotobuf-dev protobuf-compiler pkg-config nodejs gcc autoconf bison build-essential libssl-dev libyaml-dev libreadline6-dev zlib1g-dev libncurses5-dev libffi-dev libgdbm5 libgdbm-dev nginx redis-server redis-tools postgresql postgresql-contrib certbot yarn libidn11-dev libicu-dev

以上のコマンドは長いですが、間違いなくコピペして入力し、実行します。 私は、コピペの際に後ろが途切れてしまい、以下の手順でrubyコンパイルできないだのpostgresqlなんてアカウントはないだのと怒られるドジを踏みました……。

たくさんインストールするので、二分ちょっと時間がかかります。

特権ユーザーじゃなくてもできる依存関係を入れる

準備

今までの手順は、Ubuntu本体に(?)いろいろインストールしていました。マストドンでは、Rubyの特定のバージョンを使っています。こういう時に、Ubuntu本体にその特定のバージョンのRubyを入れてしまうと、ほかのバージョンのRubyを使いたいという場合、困ってしまいます。ここで、特定のバージョンのRubyを、自分でコンパイルして自分だけが使えるようにしてしまえば、その問題は発生しません。そんなことを実現するのがrbenvだそうです。

こういうとき、ユーザーを分けて行えば、何も指定しなかった時のrubyコマンドが自前コンパイルしたrubyになるようにそのユーザーの環境変数に指定しておくことで、rbenvを使っていることを意識せずに行うことができます。

というわけでユーザーを作ります。

sudo adduser mastodon

ドキュメントには書いていませんが、ここでパスワードを決める必要があります。以降使わないので、適当に決めます。住所とかわけのわからないことを聞かれますが、単にエンターを押すだけでスキップできますので、飛ばします。 ユーザーmastodonとしてログインします。

sudo su - mastodon

ログインできました。続いて、次の手順を行います。ここからはsudoは必要ありません(むしろ使えません)。

git clone https://github.com/rbenv/rbenv.git ~/.rbenv
cd ~/.rbenv
src/configure
make -C src
echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bashrc
echo 'eval "$(rbenv init -)"' >> ~/.bashrc
exec bash
type rbenv
git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

rbenvをgithubから持ってきて、rubyをビルドする準備を整えました。 type rbenvを実行すると関数みたいなものが出てくれば大丈夫なのだと思います(答えが載っていないのでわからないのですが、私がやったらそうなりました)。

Ruby2.5.1を使えるようにする

Rubyコンパイルします。

rbenv install 2.5.1

コンパイルには五分ほど時間がかかります。止まっているように見えますが、ゆっくり待ちます。

rbenv global 2.5.1

どこでもruby2.5.1を使えるようにするという意味っぽいです(?)

マストドンの依存関係をインストールする

まず、マストドンリポジトリを持ってきます。ホームディレクトリの下のliveというディレクトリに入れることにします。

cd ~
git clone https://github.com/tootsuite/mastodon.git live
cd ~/live

マストドンのmasterブランチは、開発中でいろいろバグが含まれているので、安定板を使います。以下のコマンドは、自動的に最新のバージョンを判定してそれを使えるようにするワンライナーになっています。

git checkout $(git tag -l | grep -v 'rc[0-9]*$' | sort -V | tail -n 1)

長いですね。今だと最新版はv2.4.3なので、実質的にはgit checkout v2.4.3と同じになります。

以下のコマンドを入力して、依存関係をいろいろインストールします。gemとかbundleが何をやっているのかは知りませんが、まぁなんかうまい具合に依存関係を解決してくれているようです。

gem install bundler
bundle install -j$(getconf _NPROCESSORS_ONLN) --deployment --without development test

-j$(getconf _NPROCESSORS_ONLN)の部分は、並列化オプションだと思われますが、t2.microのインスタンスだと1coreしか使えないので、指定する意味ないような気がします。 大体三分くらいかかりました。

yarn install --pure-lockfile

これには一分くらいかかりました。ところで、以下のように表示される場合、yarnのインストールをミスっています(先ほど書いた、私の踏んだドジです……)。

Usage: yarn [options]

yarn: error: no such option: --pure-lockfile
mastodonユーザーからubuntuユーザーに戻る

この後の作業は、また特権が必要な作業になります。そこで、以下のコマンドでubuntuユーザーに戻りましょう。

exit

サーバーのいろいろな設定

PostgreSQLの設定

PostgreSQLというのは、データベースを管理するソフトウェアです。マストドンのいろいろなデータをうまいこと保存してくれるようです。その設定を行っていきます。

sudo -u postgres psql

postgresというユーザーとして、psqlコマンドを実行するという意味っぽいです。すると、いつもと違ったプロンプト(入力待ってますマーク)が出ますので、

CREATE USER mastodon CREATEDB;

と入力します。これで仕事は終わりで、このプロンプトから脱出するために

\q

と入力してもとのbashに戻ってきます。

nginxの設定

nginx(えんじんえっくす、と読むらしい)は、webサーバーのソフトウェアです。HTTPSアクセスが来たら何を返すべきか、とかそういったことの設定を行います。

sudo nano /etc/nginx/sites-available/example.com.conf

でその設定ファイルを開きます。そのあと、以下のテキストを入力します。ただし、 example.comになっている部分は、全て自分のホスト名に置き換えてください。四か所あります。

map $http_upgrade $connection_upgrade {
  default upgrade;
  ''      close;
}

server {
  listen 80;
  listen [::]:80;
  server_name example.com;
  root /home/mastodon/live/public;
  # Useful for Let's Encrypt
  location /.well-known/acme-challenge/ { allow all; }
  location / { return 301 https://$host$request_uri; }
}

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name example.com;

  ssl_protocols TLSv1.2;
  ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
  ssl_prefer_server_ciphers on;
  ssl_session_cache shared:SSL:10m;

  ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
  ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

  keepalive_timeout    70;
  sendfile             on;
  client_max_body_size 8m;

  root /home/mastodon/live/public;

  gzip on;
  gzip_disable "msie6";
  gzip_vary on;
  gzip_proxied any;
  gzip_comp_level 6;
  gzip_buffers 16 8k;
  gzip_http_version 1.1;
  gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

  add_header Strict-Transport-Security "max-age=31536000";

  location / {
    try_files $uri @proxy;
  }

  location ~ ^/(emoji|packs|system/accounts/avatars|system/media_attachments/files) {
    add_header Cache-Control "public, max-age=31536000, immutable";
    try_files $uri @proxy;
  }
  
  location /sw.js {
    add_header Cache-Control "public, max-age=0";
    try_files $uri @proxy;
  }

  location @proxy {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Proxy "";
    proxy_pass_header Server;

    proxy_pass http://127.0.0.1:3000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  location /api/v1/streaming {
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header Proxy "";

    proxy_pass http://127.0.0.1:4000;
    proxy_buffering off;
    proxy_redirect off;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;

    tcp_nodelay on;
  }

  error_page 500 501 502 503 504 /500.html;
}

保存してからエディタを終了したいので、下の説明を見ます。^OでWriteOutだと書いてありますね。これは、コントロールキーとOキーを同時に押してくださいという意味です。

cd /etc/nginx/sites-enabled
sudo ln -s ../sites-available/example.com.conf

よくわかりませんがシンボリックリンクを作成して、完了です。

Let's encryptを利用してSSL証明書を取得する

HTTPSでセキュアな通信をするためには、SSL証明書が必要です。Let's encryptを使うと手軽にSSL証明書を取得できます。 まず、nginxが動いている状態では行えない(止めてくださいと怒られる)ので、以下のコマンドで停止させます。

sudo systemctl stop nginx

次に、以下のコマンドでSSL証明書を取得します。もちろん、example.comの部分は作ろうとしているマストドンインスタンスのホスト名に書き換えてください。

sudo certbot certonly --standalone -d example.com

まず、メールアドレスを聞かれるので、マストドン用に使いたいメールアドレスを入力します(私はgmailアカウントを一つ作成しました)。規約に同意するかを聞かれますから、同意する場合はAを入力します(同意しないとSSL証明書は手に入りません)。宣伝メール(?)を送っていいか聞かれますが、ここはYでもNでも問題なさそうです。これでSSL証明書が手に入り、適切な位置に配置されます。

sudo systemctl start nginx

さっき停止させたnginxを再開させます。もし、ここで変なエラーが出る場合、先ほど作ったnginxの設定ファイルが間違っている可能性があります(たとえば、example.comのままだったりするとそうなります)。

sudo certbot certonly --webroot -d example.com -w /home/mastodon/live/public/

証明書を更新するか聞かれます。公式ドキュメントによれば、ここは更新する(2を入力する)を選んでくださいと書いてありますが、更新しない(1を入力する)を選んでもうまく動きました。ちなみに、更新するのは一週間に五回くらいしか行えないらしいので、変なことになったからまた一からやり直そうみたいなことを繰り返しやっているとエラーになります。

SSL証明書の更新を自動化する

Let's encryptで手に入れたSSL証明書は、90日で失効してしまい更新が必要になります。いちいち手作業でやるのは面倒ですので自動化させましょう。定期的にコマンドを実行してくれるcronを使います。まず以下のように設定ファイルを編集します。

sudo nano /etc/cron.daily/letsencrypt-renew

ここに以下に示したような、自動実行すべきスクリプトを入力し……

#!/usr/bin/env bash
certbot renew
systemctl reload nginx

さっきと同じように保存してエディタを終了させます。ところで、このスクリプトの中にはsudoが書かれていません。これで大丈夫なのかという感じですが、crontabではなく直に作成しているので大丈夫なんだと思います(たぶん……)。crontabを使った場合はそのユーザーの権限で実行されるため、sudoが必要です。

sudo chmod +x /etc/cron.daily/letsencrypt-renew
sudo systemctl restart cron

先のスクリプトファイルに実行権限を与え、cronを再起動しておきます。

マストドンの設定を行う

またmastodonユーザーになりましょう。

sudo su - mastodon
全体の設定

以下のコマンドで、いろいろな設定を行ってくれるウィザードが出てきます。

cd ~/live
RAILS_ENV=production bundle exec rake mastodon:setup

Domain nameのところは、ホスト名を入れます。

Do you want to enable single user mode?のところは、「おひとり様インスタンス」にしたいかを聞かれていますので、Y/Nで答えます。私はNを選んだので、radon.lpha-z.netは私以外でも登録できます。

Are you using Docker to run Mastodon?のところは、Docker上で動かすかを聞かれています。どちらがいいのかわからないので、Docker上で動かさない(N)を選びました。

以下、PostgresSQL(データベース)とRedis(キャッシュらしい)の設定が始まりますが、デフォルトの設定で問題ありません。エンターを押せばデフォルトの設定になります。合計7回エンターを押します。

Do you want to store uploaded files on the cloud?の部分では、画像などを外部サーバーに保存するかを聞かれていますが、設定も面倒なのでローカルに保存することにします。Nを入力しました。

Do you want to send e-mails from localhost?の部分では、メールをこのサーバーから送るかを聞かれています。以下の説明はかなり怪しいです。

どうもEC2のサーバーからメールを送信するのは、スパムメールの問題から面倒事が多いらしいです。そこで、自前のgmailアカウントから送信することにします。

  • SMTP server: smtp.gmail.com
  • SMTP port: 587 (デフォルトでOK)
  • SMTP username: example@gmail.com
  • SMTP password: (見えませんが、入力します)
  • SMTP authentication: plain
  • SMTP OpenSSL verify mode: none
  • Email address to send e-mails "from": (デフォルトでOK)

上のような設定でうまくいきました。ただし、gmailの設定で「安全性の低いアプリからのアカウントへのアクセスを許可する」をオンにしておく必要があります。また、Let's encryptの証明書をとった時のメールアドレスでないと、テストメールは送信できても本番のメールが送信できないようなトラブルが発生しました(原因はよくわかりませんが)。そもそも、plain, noneになっているのって本当はやばい設定になっているんでしょうか……?あと、メールのパスワードが設定ファイルに平文で書かれるのはちょっと気持ち悪い……。

この設定をした後、テストメールを送るか聞かれますので、適当なメールアドレスを入力して、送信できるかと受け取れるかを確認します。パスワードの入力があっているかを二回入力で確かめたりしていないので、これはやった方が良いです。

Save configuration?とこの設定を保存するか聞かれますから、Yと答えます。ここでNと答えると、今まで入力したのが全部破棄されて一からやり直しになるので注意しましょう。

データベースの初期化

Prepare the database now?と聞かれるので、Yと答えてデータベースを初期化しましょう。ところで、いったんデータベースを作った後、諸事情があって一からやりなおしたばあい、既にデータベースがあるということで、上書きされるのではなく、エラーが出ます。その場合は、RAILS_ENV=production bundle exec rake mastodon:setupではなく、RAILS_ENV=production DISABLE_DATABASE_ENVIRONMENT_CHECK=1 bundle exec rake mastodon:setupとすることで、エラーにならないで上書きすることができます。

JavascriptCSSコンパイル

手で書いたJavascriptファイルは変数名が長く処理が遅いらしいです。そのため(?)、コンパイルを行って必要最低限のJavascriptファイルにするようです(?)。あるいは、Javascriptではない言語で書かれたコードをJavascriptに変換することをコンパイルと呼んでいるのかな?いずれにせよ、コンパイルをやるらしいです。これには五分くらいかかります。メモリがたくさん必要になります。t2.microだと最初のスワップファイル作成を忘れているとコンパイルできないかも。

Adminアカウントの作成

マストドン上でのAdminアカウントを作成します。アカウント名とメールアドレスを入力すると、仮のパスワードが出てくるので、これを使ってログインすることができます。メールアドレスはログインするときに必要になります(ログインするときに必要なのはアカウント名ではないです)。仮のパスワードは、初回ログイン後に変えておきましょう。

これで設定はおしまいです。特権ユーザーに戻りましょう。

exit

マストドンのサービス用のデーモンを作る

マストドンのウェブページにアクセスしたときには、単に静的なページを表示するだけではなく、いろいろな仕事をする必要があります。たとえば、トゥートしたからそれをデータベースに書き込んで、とかいう要求が一例です。そういった要求を待ち構えているプロセス(デーモン)を作る必要があります。

まず、

sudo nano /etc/systemd/system/mastodon-web.service

で設定ファイルをエディタで開き、以下の内容を書き込んで保存。

[Unit]
Description=mastodon-web
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="PORT=3000"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec puma -C config/puma.rb
ExecReload=/bin/kill -SIGUSR1 $MAINPID
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

同様に、

sudo nano /etc/systemd/system/mastodon-sidekiq.service

で以下を入力して、

[Unit]
Description=mastodon-sidekiq
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="RAILS_ENV=production"
Environment="DB_POOL=5"
ExecStart=/home/mastodon/.rbenv/shims/bundle exec sidekiq -c 5 -q default -q mailers -q pull -q push
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

さらに

sudo nano /etc/systemd/system/mastodon-streaming.service

で以下を入力して

[Unit]
Description=mastodon-streaming
After=network.target

[Service]
Type=simple
User=mastodon
WorkingDirectory=/home/mastodon/live
Environment="NODE_ENV=production"
Environment="PORT=4000"
ExecStart=/usr/bin/npm run start
TimeoutSec=15
Restart=always

[Install]
WantedBy=multi-user.target

以上の三つの設定ファイルを作りました。

これらを以下のコマンドで有効にします。

sudo systemctl enable /etc/systemd/system/mastodon-*.service

で、これらを開始します。

sudo systemctl start mastodon-*.service

以下のコマンドで、動いているかを確認します。

sudo systemctl status mastodon-*.service

なぜか何も出てきません。

これはUnixの「何も出てこなければ正常」ではありません。実際に動いていません。

これはよくわからないのですが、以下のように個別に開始することで、動くようになりました。なんで?

sudo systemctl start mastodon-web.service
sudo systemctl start mastodon-sidekiq.service
sudo systemctl start mastodon-streaming.service

古い画像とかのキャッシュを消す

他のインスタンスから流れてきたトゥートに添付されている画像や動画は、自分のサーバーにキャッシュされますが、延々たまっていくと邪魔なので、それを定期的に削除するcronを書いておきましょう。 crontabで作ったスクリプトは、設定を作ったユーザーの権限で行われます。

sudo su - mastodon
crontab -e

でエディタの選択肢が出るので好きなエディタを選んで

RAILS_ENV=production
@daily cd /home/mastodon/live && /home/mastodon/.rbenv/shims/bundle exec rake mastodon:media:remove_remote

を入力して保存して終了します。


以上の手順で自分のマストドンインスタンスを立てることができるはずです。

公式のドキュメントがもれなく書いてあるので、それを読めばネットワークなどの知識がなくても動かすことができます。

マストドンを使った雑感とか、改造した部分とかについても書こうと思ったのですがまた別のブログ記事で書くことにします。

あとさみしいので私のマストドンインスタンスにも遊びに来てください……。

radon.lpha-z.net

*1:Amazon Certificate Manager(ACM)で証明書を発行するとき、Aレコードがないといつまでも証明書が発行されなくて、悩みました

「レズと青い鳥」を読んだ

「レズと青い鳥」というのは、mikutterの薄い本制作委員会が今回の夏コミで頒布していた同人誌のこと。

brsywe.hatenadiary.com


今日は私がTwitterに登録してちょうど6年らしい。


通販されるのだしわざわざ買いに行かなくてもいいかな、と思っていたのだけれど、UserStreamが廃止されてから読むのはなんか違うなとも思ったし、せっかくだから買いに行くことにした。






私はいまだにスマートフォンを持っていないので、ほとんどのクライアントは使ったことがない。


でも、Shooting Starの章を見たときは何か「あの時代」が脳裏に浮かんだような気がする。


私の経験を少し書いてみる。






最初はtwitbeam[ツイットビーム]というクライアントを使っていた。

これは、ドワンゴが作っていたクライアントで、UIがとても好きだった。でも、2013/2/28でサービスを終了してしまった。

ちょうどAPI1.1に切り替わるのが三月だったので、twitbeamはXAuthを使っていたし、その影響なのだろう。


私がTweetのことを「ついっと」とよんでいるのもこのクライアント名が原因のような気がする。


ShootingStar for FeaturePhoneというクランアントも愛用していた。

Shooting Starの影響か、いわゆるUnicode文字を使った"キチ顔文字"が流行っていた時代、Unicode文字の含まれたツイートを画像で表示してくれるという機能はとても助かるものだった。

ガラケーUnicode文字に対応していないため、そういった文字は、が「ガラケーの」ハートの絵文字に変換されるといったごく一部を除けば、?に変換されてしまっていた。

キチ顔文字のパターンはそんなに多くないので、文脈から推測できるものの、キチ顔文字を使ったコミュニケーションの「雰囲気」を再現できるクライアントはこれしかなかった。

それ以外にも、APIリミットが事実上無制限とか、キチ顔文字を入力できるエスケープがあるとか、そこには求めるものがすべてあった。


でも、2013/10/12には星になってしまった。






API1.1への移行の時から、Twitterサードパーティーアプリを締め出そうとしているという風潮があった。たとえば、Display Requirementとか。

切り替わってすぐ、Shooting Starが10万アカウント制限に引っかかって、星になってしまったりしていた。


RestAPIが15分に15回というのはUserStreamが使えればあまり問題にはならなかった(UserStreamが使えないガラケーでは困ることになるけれど、ShootingStar for FeaturePhoneを使えばAPI制限は事実上回避できた)し、

絶対時間じゃなくて相対時間で示せ、っていうのがDisplay Requirementに含まれていて、わかりにくくなった(一時間前、の幅が広すぎていつだかわからない!)以外にはそれほど締め出されている感じはなかった。その時は、まだ。


複数人DM機能やアンケート機能のAPIが公開されないなど、そういった方面でサードパーティークライアントアプリと差別化していくのかなとも思っていたけれど、結局UserStreamAPIを廃止することに踏み切ってしまった。


公式によれば、1%しかUserStreamを使っていないらしい。ライト層というか、サイレントマジョリティ的な感じで、それ自体は正しいのだと思う。

でもやはり、そういった時代を作ってきたサードパーティークライアントアプリ製作者に、恩をあだで返すような仕打ちに思える。






読んでいて、ツイッタークライアント製作者って大学生が多かったのだなぁと思った。それと同時に、自分もそういうことをやってみたかった、でももうそんなことはできない、という気持ちにもなる。


提供されているものを取り上げられたからいじわるだ、という考え方ではなく、いままで与えられていたことに感謝して、時代が変わったと受け止めるべきなんだろう。





スマートフォンの普及の歴史、マイクロブログの隆盛の歴史、Twitterの歴史。


そういった時代を思い出すことができた。



すごくいいものを読ませてもらったと思う。


ありがとう、サードパーティークライアント製作者たち、さよなら、UserStream。



C++で安全に絶対値を求める

C++で安全に絶対値を求める方法について、調べても出てこないので書いておきます。 安全に、というのは「オーバーフローなどの未定義動作を起こさず」という意味です。

#include <type_traits>
#include <cstdint>

template<class T, class U = std::make_unsigned_t<T>>
U SafeAbs( T x ) {
    return x < 0 ? -static_cast<uintmax_t>(x) : x;
}

解説

std::make_unsigned_t<T>は見慣れないですが、単にTの符号なしバージョンを持ってこれるメタ関数です。たとえば、std::make_unsigned_t<int>unsigned intになります。 Tが符号付整数の場合、その絶対値はTで表せるとは限らないので、符号なしバージョンを使わないと安全ではありません。 実際、負の数を二の補数で表現している処理系では、INT_MINの絶対値はintに収まらないことが問題となります。

まず、xが非負ならば自明にそのまま返せば問題ありません。以下、負の場合に何をやっているかを説明します。

uintmax_tも見る機会があまりありませんが、符号なし整数のうち最も大きいものです。 -xとしてしまった瞬間にオーバーフローの危険性があるので、まずは符号なし整数に変換します。 符号なし整数に変換されるときは、負の数であれば2Nを加えると決まっています。ここで、Nはその符号なし整数(ここではuintmax_t)のビット幅です。

次に、uintmax_tにキャストしたものに単項負符号演算子を適用しています。符号なし整数に単項負符号演算子を適用するのは一見するとおかしなことになりそうですが、問題ありません。 このような場合、数学的な結果を2Nで割った余りが得られることに決まっています。そのため、数学的には、-(x+2N) mod 2N を計算していることと等価になり、結局-xが得られます。

最後に、なぜuintmax_tを使っているかについて説明します。この部分は、Uでも問題なさそうに思われます。しかし、Uを使う場合、移植性をはばむ、C++における二大問題を避けることが困難になります。

  • 符号付整数のビット表現
  • 汎整数昇格

そんな処理系はまずありえないのですが、C++においては符号付整数の表せる範囲は物凄く偏っている可能性があります(C言語では、必ず「二の補数」「一の補数」「符号と絶対値」しかありえません)。 intは符号付整数ですので、やはりものすごく偏っている可能性があります。 たとえば、intの表せる範囲が-1073741824~3221225471という可能性もあります。

Uにキャストした場合、単項負符号演算子を適用する前に汎整数昇格が発生するかもしれません。 汎整数昇格が発生するのは、Uで表せる値がすべてintでも表せる場合ですので、この部分でオーバーフローが発生することはありません。 しかし、前述のようにintで表せる範囲が偏っている場合、intに昇格した値に単項負符号演算子を適用した結果がintで表せる保証はありません。

uintmax_tを用いた場合、intに昇格する可能性はないため、この問題は発生しません。

重箱の隅をつつくようですが、こういうパターンがあるためにuintmax_tの代わりにUを使うのは真の意味で「安全」とは言えません。とはいえ、むしろそのような処理系があれば教えていただきたいくらいのレベルで珍しい処理系ですが……。

速度について

ちなみに、安全性は気にしないから速度を重視したいという方もいらっしゃると思います。少なくともx86のネイティブコードを出力するまともなコンパイラであれば処理速度的にそれほど劣ったコードにはなりません。

gccでは、uintmax_tであってもUであってもビット演算を用いた条件分岐のないコードになりました。clangでは、uintmax_tであってもUであっても、char以外はcmov命令を用いたコードになり、cmov命令が使えないcharの場合はgccとほぼ同様のコードになりました。

ちなみに、charshortの場合はキャストをしなくても安全です。clangでは変化しませんが、gccでは特定のパターンであることを見抜いたのか、cdq命令を使うようになります。

一方、intlong longの場合、キャストをしないとソースコード上に潜在的に未定義動作を含むことになります。ただし、キャストをしない場合とコンパイラの出力するコードは変わりません。 この場合、結果は望むものになるとはいえ、ソースコード上は潜在的に未定義動作を含んでいます。

ちなみに、以下のように(移植性はないが、その処理系にとっての)未定義動作を排除したコードでは、コンパイラは条件分岐を出力するようです。 なお、gccかつlong long版のみ、後半部分を見抜いてcqo命令を使うようになりました。なぜintの時はcdqにならないのか、x == LLONG_MINの分岐がないとcqoにならないのかは不明です。

unsigned long long Abs( long long x ) {
    return x == LLONG_MIN ? 1ull<<63 : x < 0 ? -x : x;
}

ただし、これらの結果は関数単位でのコンパイル結果を見ている点については注意が必要です。呼び出し規約上、charshortの引数は符号拡張されてから関数に渡されます。 実際にはこのような関数はほぼ確実にインライン展開されるため、符号拡張命令分のコストを考える必要もあります。

参考

絶対値以外の、一般の演算についてその安全性を議論した記事として、

C++ における整数型の怪と "移植性のある" オーバーフローチェッカー (第1回 : 整数型の怪と対策の不足)

があります。落とし穴が詳しく解説されているためおすすめです。

constexprで書けない純粋な関数

今週はThe continuing evolution of C++の講演を聞いてきました。C++20では

  • concept
  • module
  • contract

を入れるというような話もしていました。どれもよい機能です。conceptは入ったらいいと皆が思うからこそ、微妙な点で意見が食い違ってなかなか入らないのですよね。さすがにそろそろ入ってほしいです。moduleはあまり話題になっていないので知らなかったです。コンパイルが速くなる誰もがうれしい機能のはずですが、つまみ食い的に使って楽になるような機能ではないことが話題に上らない理由なんでしょうか。inline変数などヘッダ前提の機能も着実に追加されていったりしていますし、ヘッダスタイルはまだまだ残りそうです。contractはプログラミングパラダイムを変えうるわけですが、だんだんと追加していくタイプの機能になるのでしょうか。


さて、先週は副作用を利用して関数の中身がどうなっているかを調べる話を書きました。

この方法に類似した面白い話題があります。それは、純粋関数型言語で定義できない純粋な関数 - sumiiの日記という問題です。純粋という単語の使われ方が紛らわしいですが、

  • 純粋関数型言語→副作用のあるような機能を実現する構文がない関数型言語
  • 純粋な関数→(内部で副作用を使ってもいいが)参照透明な関数

という意味です。

constexprは、C++11の段階では確かに純粋関数型言語でした。 しかし、C++14では関数内で完結する副作用が許されたため、あくまで純粋な関数しか書けないとはいえ、純粋関数型言語では書けないような関数が書けるようになったのです。C++17でラムダ式の呼び出しもconstexpr指定されるようになったため、さらに書きやすくなりました。

では実際にC++11では書けなかったような関数を書いてみましょう。

……

実際に書いてみるとわかりますが、普通にやってもこのようなことはできません。

局所的にせよ副作用のあるようなラムダ式はなんらかのキャプチャをしているはずなので、関数ポインタに変換できません*1

つまり、「関数ポインタをとれる関数」かつ「constexpr文脈で評価できる関数(static変数とかを持っていない)」の範囲であれば、副作用を許したところで表現力は変わらないように思われます(本当にそうなのかよくわかっていません)。

ラムダ式を引数に取れるよう、関数ポインタ以外に関数オブジェクトもとれるようなテンプレート関数にするとどうでしょうか。

実際に書いてみたのがこちらです。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

BOTTOM、つまり無限ループになることを確かめる方法は原理的にないのですが、このくらいの単純な関数なら、再帰深度制限で落ちれば無限ループと判断してよいでしょう。

……というのはちょっと罠で、clang++でstatic_assert( f() == nullptr )と書いてしまうと、f()の型はstd::nullptr_tだから恒真、と判断してしまい、関数呼び出しすらしないようです。

g++で確かめることにしましょう。

ところであまり素直ではない書き方になっているのが気になります。

先ほど書いたように、関数ポインタを受け取る関数を作るのでは、ラムダ式を受け取ることができなくて詰みます。

そうするとジェネリックラムダで受け取りたくなるわけですが、自己再帰関数関数が書けなくて詰みます*2

自前でtemplate<...>と書けばいいのかというとそうでもなくて、関数テンプレートは、テンプレート引数を指定せず関数の引数に入れることができなくて詰みます*3

というわけでテンプレートなoperator()を持った関数オブジェクトを作っているわけです。自己再帰(*this)(unit)みたいになっていて微妙ですが、仕方ありません。

ただ、先のコードでは、C++14以降でしか書けないようなconstexpr関数があることを示したことにはならないような気がします。 オーバーロード関数を作ってしまえば、引数に与えられる関数ごとに挙動を変えることは非常に簡単です。 そういうような書き方をするとその多相関数は「一様」でなくなってしまうのですが、C++11でも書けるには書けるということです。

結局、C++14になって局所的な副作用が許されたからといってconstexprで書ける関数の範囲は変わらないということなんでしょうか。 関数ポインタをとれるという条件(あるいは、ヒープが使えないという条件下での型システム)によって、純粋関数型言語で書ける関数に限定されてしまうというのは不思議な気がします。

*1:C++的には、ただの関数と情報を持っているクロージャーでは当然サイズが違うので同じ型を付けられません(C++言語での型というのにはストレージをどれだけ使うかというのまで含まれてしまいます)。サイズが違っても同じ型に見せるには、動的確保が必要ですが、constexpr文脈では残念ながらできません。

*2:let recみたいなのがなく、ラムダ式の中で自己再帰する方法がありません。自前でfixみたいなものを作ればできないこともないですが、見た目がまるっきり変わってしまいます。

*3:関数テンプレートはあくまでテンプレートであり、C++の世界の値ではありません。つまり、多相関数をそのまま受け取る方法がありません。ランク1多相が偽物なのでランク2多相が書けなくなってしまっています。

ISAシミュレータを書くのを楽するためにTMPを使う方法

半年くらいブログをほったらかしにしていました。おひさしぶりです。 ついったーにC++のコードを張り付けていても検索性が低いなぁと思ったので今後はブログにまとめていこうかなぁという感じです。

ISAシミュレータを作るとき、一つ一つの命令の定義は非常に単純なのに、付加的な情報を定義しなければいけないために冗長になってしまうことがよくあります。 たとえば、レジスタの値をストアするSTORE命令を考えてみると、

mem[src[1]+imm[0]] = src[0];

くらいの情報量で充分に定義できているはずです。 しかし、ソースレジスタが2個だとか、デスティネーションレジスタはないだとか、そういった情報も使いたくなることがあります。 そういった情報は先ほどの定義から原理的には抽出できるはずですが、C++コードで行う方法は必ずしも自明ではありません。 また、ISAシミュレータは速いことが望まれますので、出来ることなら実行時に動的に判断するのではなく、コンパイル時に静的に決まってほしいです。 こういった情報を別の場所に手書きするのは、保守性が低下するのであまりやりたくないところです。

テンプレートメタプログラミングを使うとそういったこともできるということを示すためにちょっと書いてみました。

[Wandbox]三へ( へ՞ਊ ՞)へ ハッハッ

ltmpcほどテンプレートメタプログラミングという感じではなく、constexpr要素も含んでいます。

各命令の動作の定義には、ジェネリックラムダを使います。auto型の引数の部分は実質テンプレートなので、あえて変な型のオブジェクトを入れることもできます。 ここで、入れることのできる変な型のオブジェクトは、単に同じ名前の変数や関数が定義されていれば問題なく動きます(いわゆるダックタイピング的なことができます)。

これを利用(悪用ですね)して、ラムダ式の中でどのような変数にアクセスしているかを検出するオブジェクトを作ることができます。 これをラムダ式の引数に与えることで、ラムダ式の中での動作を調べ上げることができます。

今回は単にアクセスされるかだけの判定でしたが、演算子オーバーロード・式テンプレートと組み合わせればどういった依存関係になっているのかということも判定できそうです。 最初から式テンプレートを使えばconstexprなしの完全TMPで同じことができそうに思えますが、src[0]みたいな部分はうまく扱えないです。0_srcとかsrc<0>()とか書けば型に落とし込めますが、見た目とのトレードオフになりそうです。有限個なのでとあきらめてsrc0みたいな変数を作ってしまうのはちょっと負けた感じがあります。

ただ、こういうのを書くのは、簡単な命令の時は有効ですが、わけのわからない細かい仕様のある命令の時は結局愚直に書く必要があって、あまり楽をできなくなってしまいます。悲しい。