電柱日報

日々の由無し事

Compiler internals(拙訳)

※:この記事は,Crystal Advent Calendar 2018 の22日目です。

Crystalの公式GitHubリポジトリには, Compiler internals というCrystalコンパイラの挙動について解説されたWikiページが公開されています。元の記事が書かれたのは4年ほど前になるため,現状の実装からずれてしまっている部分も多くありますが,crystal コマンドの動きを理解する第一歩としては非常に役に立つ情報だと思いましたので,個人的な理解をベースにざっくりと日本語化してみました。

日本語としての推敲もあまりできておらず不自然な表現もありますが,少しでもお役に立てば幸いです。

なお,これを書いてる人間は翻訳の専門家ではないため,不正確なニュアンスが含まれている可能性があります。おおよその雰囲気がつかめたら,是非原文を当たるようにしてください。

また,もし興味を持っていただけたようであれば,次は現行コンパイラのソースを追いかけてみてはいかがでしょうか。


Compiler internals / コンパイラの内側

ここでは、どのようにコンパイラが動作するかを紹介します。必要に応じて関連コードへのリンクを貼るつもりですが、使用するのは このバージョン です。これは比較的最近(訳注:執筆時点(2014年12月)で)のものなので、もしコードが変更されてしまっても基本的なアルゴリズムに大きな影響はないはずです。

The main file / メインファイル

Crystalコンパイラコンパイルする際のメインファイルは src/compiler/crystal.cr です。このファイルは,Crystalに関連する全てのソースコードを require してから,Crystal::Command.run を実行しています。Command モジュールはコンパイラCUI機能を提供しており,Compiler を生成して,その設定を行い,1つ以上のソースファイルを コンパイル します。

では,Compiler クラスが何をするのかを見ていきましょう。

The Compiler class / Compiler クラス

Comliler クラスの主なパブリックメソッドは compile です。

まず最初に,Program オブジェクトがインスタンス化されます。Program は全てを収容するトップレベルのコンテナで,その内部にクラスやモジュールを定義可能なトップレベルモジュールのように働きます。Rubyputs self を実行した際の main と似たようなものですが,Ruby とは異なり,トップレベルにメソッドを追加すると,そのメソッドはこの Program 型に定義されることになります。Ruby のように Object 型にプライベートメソッドとして定義されるわけではありません。

Program のソースコードを見てもらうとわかるように,ここでは ObjectNilString といった,全てのプログラムで一般的に使用されるいくつかの基本的な型が定義されています。

また,Program はそのコンパイルに関連するデータ,例えば使用される全ての シンボル(シンボルはソースコード内で動的には生成できません)や,CRYSTAL_PATHRubyにおける $LOAD_PATH に似ていますが,変更できません) のようないくつかの設定など,のコンテナでもあります。

Compiler#compile の話に戻りましょう。Program が生成されて設定が完了すると,ソースコード構文解析(parse)ここでも) されます。構文解析によって,それぞれのファイルが AST へ変換されると,それらは 正規化 されます。正規化の処理には,AST ノードを別の形へ変形させるようなものも含まれます。こうした変換処理の一番重要なものは,require を,実際に読み込むファイルの内容に応じた AST ノードへ変換する,という処理です。その他では,例えば unless を,分岐が逆転した if に変換するなどといった処理もあります。

この段階が終了すると,require で取り込んだ内容(特別な "prelude" ファイルは,自動的に取り込まれます)を含んだプログラム全体に相当する AST ノードを手にすることになります。ほとんどの場合,このノードは単一の Expressions ノードで,Expressions とは「2つ以上の AST ノードを含んでいる」という状態を表すノードです。

次のステップは最も重要な 型推論 です。

Type inference / 型推論

この段階の名称は語弊があるかもしれません。ソースコード内では infer_type と呼んでいますが,そこではさらに多くのことが起きています。このような実装には,デメリットもメリットもあります。まずデメリットは,多くの処理が混在することでソースコードを追いかけて理解するのが困難になることです。一方のメリットは,プログラム全体を1度だけ走査すれば良いので,コンパイルが高速になることです。コンパイラを使用する開発者の方が,コンパイラ自身を開発する開発者よりも数が多く,またコンパイル時間は我々にとっても非常に重要であるため,この場合はメリットがデメリットを上回ると我々は信じています。

では,Program#infer_type(node) が何をしているのか見てみましょう。

最初に行われる,もっとも重要な処理は,AST ノードを走査するための TypeVisitor を生成することです。ここでは Visitor パターン を利用しています。この Visitor パターンは,AST ノードを処理するもっとも有用な手法の1つとして,多くのコンパイラで広く利用されています。Crystalの多重ディスパッチ機能は Visitor パターンを使用して非常に簡単に実装できたので,手動でダブルディスパッチ 処理をせずに済んでいます。

TypeVisitor は以下のような多くの仕事を受け持っています。

  • 型やメソッドを宣言する
  • 型情報を伝搬するために AST ノード同士を紐付ける
  • 変数の型とその流れを解析する

Type and methods declaration / 型とメソッドの宣言

ユーザが以下のようなコードを書いたとしましょう。

class Foo
  class Bar
    def baz
        1
    end
  end
end

この場合,Foo クラスを新たに宣言し(すでにあるなら開き直し),その中に Bar クラスを宣言して(すでにあるなら開き直して),その中に buz メソッドを宣言(すでにあるなら再定義)する必要があります。

そのために,TypeVisitor型のスタック を持っています。初期状態のこのスタックは @mod のみが要素として保持されています。

注: コード全体を通じて,mod という単語は,グローバルにアクセス可能なモジュール(module)である Program のことをさします。別の場面では program という単語が使用されることもあります。mod は以前から使われているもので,program に置き換えることもできるのですが,mod はとても短くて便利でもあります。

型のスタックの話に戻りましょう。このスタックは初期状態では Program のみが登録されています。このことは,何らかの型が定義された場合に,それらの型が Program 内に定義されることを意味しています。あるクラスの内部処理中は,その型がスタックに プッシュ され,この状態で追加される新しい型はその型の内部に定義されます。クラス内の処理が終われば,型のスタックからポップされます。この辺りの動きは visit(node : ClassDef) で確認できます。このメソッドにはそれ以外にも,各種バリデーション(例えば,親クラスに齟齬がないか,クラスをモジュールとして開き直していないか,名前空間は存在するかか,等々)や,ジェネリック型の処理,フック(inherited)の実行など,多くのコードが存在しています。

同様の処理は,ModuleEnumLibAliasIncludeExtendDefMacro といった各種定義に対しても行われています。

AST nodes binding / AST ノードの紐付け

このセクションの内容を正確に理解するためには,このブログ記事もう1つ別の記事 を読んでおくよう 強く 推奨します。

簡潔にいうと,型推論アルゴリズムは,AST ノード同士の紐付けによって実現されています。ノード A と B とを紐づけた場合,A の型は B の型になり,もし B の型が変化すれば,同じように A の型も変化します。ここで,A がさらに別のノード C と紐づけられた場合,A の型は B の型と C の型のユニオン型になります。

全ての ASTNode は,自身と1つ以上のノードとを紐づける bind_to メソッドを持っています。ノード A にノード B を紐づけた場合,B は A の dependencies に追加され,同時に B は A を observers に追加します。その結果として,A の dependencies は [B] となり,B の observers は [A] となります。

あるノードが別のノードに紐付けられると,そのノードの型はdependensies の型をマージ(marge) して再計算されます。型のマージがどのように行われるかは付録で説明します(訳注:現時点で付録は公開されていません)。新しい型が追加されると,AST ノードはそのことを 伝搬する(propagate) ために,自身の observersupdate メソッドを実行します。update メソッドでも,大なり小なり同様の処理が行われます(もしその情報を受け取っていなければ,dependencies に基づいた新しい型 を再計算するなど)。そのノードは dirty としてマークされ,全ての observers がの update が終了すると,続いてそれらの propagate が実行されます。こうすることで,情報の伝搬処理を小さくし,余計な伝搬を防止しています。

注:上記のコードは semantic/ast.cr ファイル内に記述されています。syntax/ast.cr というファイルもありますが,こちらには AST ノードとそのプロパティが定義されています。semantic ディレクトリ内のファイルには,意味解析の段階で何を行うかが収められており,AST ノードの定義を開き直して多くの機能を追加しています。このことによって,AST ノードの実装を機能によってグルーピングされた複数のファイルファイルに分割することができ,単一の ast.cr ファイルに全ての機能が混在して巨大化するような事態を防いでいます。

では,bind_to はどこで使われるのでしょうか? 以下のようなコードを想定してみましょう。

a = 1

これは代入(Assign)ノードで,やがて TypeVisitor がそのノードを 訪問(visit) することになります。すると,代入対象(式の左辺)が変数(Var)なので,このメソッド が実行されます。まず先に,代入される値が走査されます。このケースでは,値は数値リテラル(NumberLiteral)でした。数値リテラルから型を割り当てるのは簡単で,それが Int32 リテラルであれば,その型は Int32 型 になります。この型はよく知られたもので,Program 上にすでに定義されており,mod 変数を通じてアクセス可能です。これは,ノードとの紐付けが行われない数少ない例のうちの1つです。この他,NilBoolChar といったプリミティブな型も同様の動きになります。

type_assign)メソッドの話に戻りましょう。そこでは,代入対象が実施に値と紐付けられている様子が確認できます(他にも色々と確認できますが)。同時に,ノードもまた値に紐付けられて います。これは,代入(Assign)というノード(式)自体の型もまた,代入される値の型になるためです。

つづく……

Crystal でシグナル処理

※:この記事は,Crystal Advent Calendar 2018 の8日目です。

プログラムの実行中にキーボードからCtrl+Cが押されたり,バックグラウンドで動作中にkill コマンドが実行されたりした際,プログラムに対してシステムからシグナルが発行されます。

放っておけば,システム標準の動作が行われる(プログラムが強制終了したり)わけですが,時として飛んできたシグナルをトラップして処理したい状況があります。

例えば,Ctrl+C入力時に終了する前に何か後処理をしたい場合や,SIGHUPの受信時に設定を再読み込みしたいような場合などなど。

そうしたシグナル処理を安全に行うために,Crysltalには Signal 型が用意されています。

Signal

Crystal の Signal 型は列挙型(enum)として定義されており,以下のシグナルがメンバーとして登録されているほか,シグナルハンドラの設定/解除するなど,いくつかのインスタンスメソッドが定義されています。

enum Signal : Int32
  # ...
  HUP = 1
  INT = 2
  QUIT = 3
  ILL = 4
  TRAP = 5
  IOT = 6
  ABRT = 6
  FPE = 8
  KILL = 9
  BUS = 7
  SEGV = 11
  SYS = 31
  PIPE = 13
  ALRM = 14
  TERM = 15
  URG = 23
  STOP = 19
  TSTP = 20
  CONT = 18
  CHLD = 17
  TTIN = 21
  TTOU = 22
  IO = 29
  XCPU = 24
  XFSZ = 25
  VTALRM = 26
  USR1 = 10
  USR2 = 12
  WINCH = 28
  # ...
end

シグナルハンドラの設定

Signal 型の各メンバーに対して,Signal#trap メソッドを使用すると対応するシグナルの受信時の処理を行うシグナルハンドラを指定することができます。

# Ctrl+C(SIGINT)時に何か後始末(cleanup)をしてから終了する
Signal::INT.trap do
  cleanup
  exit(0)
end

# SIGHUP 時に設定を再読み込み(reload_config)する
Signal::HUP.trap do
  reload_config
end

このようにしてシグナルハンドラを設定したした場合,基本的にシグナルハンドラが未設定の場合のシステム標準動作は行われなくなりますので注意してください。

例えば,上の例の Signal::INT.trap do ... end 内で exit を呼び出さなかった場合,Ctrl+Cが押されるごとに後始末処理は行われますがその時点ではプログラムは終了しなくなります。

設定したシグナルハンドラの解除

Signal#trap メソッドで指定されたハンドラは,Signal#reset メソッドで解除することもできます。

例えば,以下のコードを実行すると,最初の10秒間はCtrl+Cを入力しても "Ctrl+C received" とだけ表示されてプログラムは終了しませんが,その後の10秒間はCtrl+Cによって通常通りプログラムが強制終了されます。

Signal::INT.trap do
  puts "Ctrl+C received"
end

sleep(10)

Signal::INT.reset

sleep(10)

puts "finish"

シグナルの無視

Signal#ignore メソッドを利用すると,該当のシグナルを受け取った際になにも処理をせず,システム標準の動作もさせず無視することができます。

以下のコードを実行した場合,何度 Ctrl+C を入力してもプログラムの動作に一切の影響を与えることはありません。

Signal::INT.ignore

# do something ...

シグナル処理の例外動作

前述した Signal#trapSignal#resetSignal#ignoreの動作にはいくつか例外が存在します。

例えば, SIGKILLSignal::KILL)はシグナルハンドラ内で明示的に exit などが呼び出されなかったとしても,そのハンドラの処理が終了すればプログラムが強制終了されます。また Signal::KILL.ignore などとしても強制終了を免れることはできません。

一方,SIGCHLDSignal::CHLD)はシグナルハンドラが設定されていたとしても,それより先にCrystalの標準的なSIGCHLDハンドラの動作(終了した子プロセスの刈り取り処理など)が実行されます。また,Signal::CHLD.resetSignal::CHLD.ignore を呼び出した場合も,このCrystal標準のSIGCHLDハンドラは実行されます。これは,Process.wait メソッドがこの標準SIGCHLDハンドラの動作に依存していることも理由の1つですが,子プロセスのゾンビ化を防ぐ意味もあります。

CrystalでC言語の関数を使う際のポインタ操作

※:この記事は,Crystal Advent Calendar 2018 の7日目です。

最近の高級言語(って今でも言うんですかね?)では,めっきり触ることが少なくなったポインタ操作ですが,Crystal で libc の関数を直接叩いたりバインディングを書いたりすると,ちょくちょくその必要にかられる場面に遭遇します。

Crystalの公式ドキュメントで 安全でない(unsafe)コード のトップに挙げられているように,どうしても必要な時以外は手を出さない方が良いのですが,C言語の関数を使おうと思うとそうも言ってはいられません。

というわけで,Crystalのオブジェクトとそのポインタとの相互変換方法や,ちょっと特殊なポインタ操作についてご紹介します。

ポインタ型

Crystalでは T 型のポインタは Pointer(T) 型と表現します。

この表記もたまに使う分には明確で良いのですが,なんども出てくると一々書くのが億劫です。また,C言語でよく目にするポインタのポインタ,例えば int ** などは,Pointer(Pointer(LibC::Int)) となってさらに面倒臭くなります。

なので,Pointer(T)T* と書けるようになっています。ポインタのポインタも T** とかけてとても楽。

でもって,あるオブジェクトをポインタに変換する場合には pointerof() を使用します。

p a = 1
#=> 1

p typeof(a)
#=> Int32

ptr_a = pointerof(a)

p typeof(ptr_a)
#=> Pointer(Int32)

逆に,ポインタオブジェクトの #value メソッドを使用すると,そのポインタが指し示すオブジェクトを取得することができます。

# 上のコードの続き

p b = ptr_a.value
#=> 1

p typeof(b)
#=> Int32

この時,変数 ab は同じメモリを参照しています。

上記の例にあるプリミティブな数値型のように,自身を破壊的に変更することがない場合はさほど問題になりませんが,Crystalの一般的なオブジェクトで同じような事をすると,オブジェクトの状態が想定外に変化する場合があるため注意が必要です。

c = [0, 1]

ptr_c = pointerof(c)

d = ptr_c.value

c << 2

p d
#=> [0, 1, 2]

あるポインタを別の型のポインタとして扱う

C言語では,まれに(よく?)ある型のポインタとして宣言された変数を別の型のポインタにキャストして使う,といった場面があります。

例えば,LinuxとMac OS XでMACアドレスを取得する方法 で紹介されているMac版サンプルコードの19行目は以下のようになっています。

dl = (struct sockaddr_dl*)ifa->ifa_addr; 

変数 ifa はその上の11行目で,ifaddrs 構造体として宣言されており,その ifa_addr メンバは本来 sockaddr 構造体のポインタであるはずです。しかしここでは,その ifa_addr メンバを sockaddr_dl 構造体のポインタであるものとして変数 dl へ代入しており,以降dlsockaddr_dl 構造体として利用しています。

ifaddrssockaddrsockaddr_dl 構造体をそれぞれ LibC::Ifaddrs 型,LibC::Sockaddr 型,LibC::Sockaddr 型としてバインディングが定義されていた場合に,同様の操作をするには以下のような処理になります。

# ifa : LibC::Ifaddr
# ifa.ifa_addr : LibC::Sockaddr*
dl = ifa.ifa_addr.as(LibC::SockaddrDl*).value
p typeof(dl)
#=> LibC::SockaddrDl

Crystalの情報を得るには(2018年版)

※:この記事は,Crystal Advent Calendar 2018 の6日目です。

2018年末時点で利用可能な,Crystalに関連した情報を取得できるサイトをまとめてみました。

何だかんだで英語で得られる情報の方が充実していますが,1.0に向けて開発も佳境を迎えそうな2019年,Crystalに注目してみてはいかがでしょうか。

注:Crystalは現在も開発が進められており,時折言語仕様に破壊的な変更が加わる場合があります。書かれてから時間が経ったコンテンツは,その内容と現在の実装との間で齟齬が生じている可能性がありますのでご注意ください。

日本語の情報

Introducing Crystal Programming Language

日本のCrystalユーザグループ Crystal-JP が技術書典5で刊行した入門書のWeb版。

Crystalを使ってみたいと思っている方向けの,比較的最近の情報がまとまっています。

Slack

Crystal-JPが運用しているSlack ワークスペース

日本語で質問すると,だれかが答えてくれるかもしれません。

実は海外のCrystal関連の大御所がこっそり(?)参加していたりもします。

過去のAdvent Calendar

実は2015年から行われている Crystal の Advent Calendarの過去ログ。

すでに情報として古くなっている場合もありますのでご注意ください。

Qiita

ここで crystal タグを漁ると,日本語で書かれた関連記事が見つかります。

Crystalのシンタックスハイライトに対応してくれていないのが唯一の難点。

Compass

IT勉強会のスケジューリングや参加登録ができるWebサービス

Crystal勉強会が開催される際は,大抵ここで募集がかかります。

過去の勉強会ページでは,当日の発表資料も公開されています。

英語の情報

公式サイト

Crystal言語の総本山。

ここから,後述する公式ブログ,ドキュメント,APIリファレンス,GitHubリポジトリ,公式フォーラム,その他へ飛ぶことができます。

公式ブログ

公式サイト内のブログ。

更新頻度はあまり高くありませんが,新バージョンのリリースノートやコンパイラの実装に関するコアな解説,Crystalを採用したプロジェクトの紹介記事などが上がっています。

公式ドキュメント

各環境への Crystal コンパイラのインストール手順から,基本的な型や文法の紹介,さらにはマクロやCバインディングといった中級者以上向けの内容までをザックリと網羅。

英語に負けず,まずここを読んでみるのが取っ掛かりには近道かも。

APIリファレンス

Crystalコンパイラに標準添付されている型やメソッドを参照可能。

Crystalでプログラミングを初めてから,なにか困ったらココを見にくるのが吉。

GitHubリポジトリ

Crystalコンパイラの開発拠点。

Crystalコンパイラ自身がCrystal言語で書かれているので,膨大なサンプルコード集としても利用できるありがたい存在です。

前述のAPIリファレンスから,GitHubリポジトリ上のメソッド実装箇所へ飛ぶリンクなどもあり,メソッドの挙動がよくわからない時などはソースを当たると結構解決したりします。

また,盛り上がってるIssueやプルリクなどをチェックしすると,コンパイラ界隈の動向が見えて面白かったりも。

メーリングリスト

Google Groups上で運用されている公式のメーリングリスト

コアチームの面々に直接質問が出せたりします。

次項の公式フォーラムが始まったことで,これまでメーリングリスト上で行われていたコミュニケーション部分の機能は最終的には公式フォーラムへ移行し,こちらは現行コンテンツのアーカイブになる方向性が示されました。

We are launching a discourse forum at https://forum.crystal-lang.org.

We will like to encourage that future conversation happen there and in the future the mailing list will stay as an archive.

[ANN] forum.crystal-lang.org

公式フォーラム

つい最近,今月(2018年12月)になって公開された公式のフォーラム。

メールアドレスでユーザを登録するか,GitHubアカウントと連携させて参加できます。

Twitter

Crystal言語の公式Twitterアカウント。

新バージョンの告知や,Crystal関連の情報がつぶやかれます。

Gitter

Gitter上のCrystalチャンネル。

かなり活発に発言が飛び交っており,公式フォーラムは掲示板,Gitterはチャットといった使い分けになっているようです。

IRCボットも動いていて,irc.freenode.net 上の IRCチャンネルへの発言と相互連携しています。

Stack Overflow

プログラミング関連の質問をすると誰かが答えてくれる互助会的なサイト。

ここでも結構コア開発者から回答をもらえたりします。

Reddit

ニュース記事、画像のリンクやテキストを投稿し、コメントをつけてリスト化できるWebサイト。

Reddit にもCrystal関連の情報がまとまっており,自分の作った shard(Ruby でいう gem のような外部ライブラリ)などをここで宣伝することもできます。

awesome-crystal

一定の条件を満たした自薦他薦の shard がカテゴリ分類してリスト化。

リストの更新も人の手で管理されているため、限定的ではありますが,ある程度の基準をクリアした shard が見つかります。

CrystalShards.xyz

GitHub で公開されている shard のキーワード検索機能を提供。

GitHub で公開されている shard を自動的に収集しているため玉石混交ではあるものの、Awesome Crytstal より網羅的にマイナな shard も見つけることができます。

Crystal Weekly

Crystal 関連のメールニュース。

メールアドレスを登録しておくと,不定期にCrystal関連の情報が配信されます。

Crystal [ANN]

Crystal関連のニュースをまとめたWebサイト。

Crystal関連の新しいプロダクトの情報がちょこちょこ更新されています。

指定したネットワークインタフェースのMACアドレスを取得する

※:この記事は Crystal Advent Calendar 2018 の4日目です。

以前から,crystal-emailcrystal-patliteなど,CrystalでTCPベースのネットワークプログラムをチョコチョコと書いていましたが,ここのところネットワーク熱が再燃しています。

直近だと,ICMP Echoを投げてPingコマンドもどきの動作をするping.crとか。

まぁ,完全に知的好奇心でチャレンジしているので,実用性については深く追求しない方向でお願いします。

で,今回はなんとなくCrystalのプログラム中で,ハードウェアに搭載されたネットワークインタフェースのMACアドレスを取得することに挑戦してみました。

現状Linuxでしか動作確認が取れていませんが(MacOS 10.14だとエラーになる),一応の形になったコードがこちら。

# get_mac.cr
require "socket"

lib LibC
  IF_NAMESIZE   =     16
  IFHWADDRLEN   =      6
  SIOCGIFHWADDR = 0x8927

  # if_req構造体の簡易版
  struct IfReq
    ifr_name : StaticArray(Char, IF_NAMESIZE)
    ifr_addr : Sockaddr
  end

  # ioctl関数の定義
  fun ioctl(fd : Int, request : Int, value_result : Void*) : Int
end

unless ARGV.size == 1
  STDERR << "usage: get_mac IF_NAME\n"
  exit(1)
end

if_name = ARGV.first

unless if_name.size < LibC::IF_NAMESIZE
  STDERR << "Error: IF name too long.\n"
  exit(1)
end

# ioctl関数に渡すソケットを用意
socket = LibC.socket(LibC::AF_INET, LibC::SOCK_DGRAM, 0)

# if_req構造体を生成
ifreq = LibC::IfReq.new

# if_req構造体にアドレスファミリを設定
ifreq.ifr_addr.sa_family = LibC::AF_INET

# if_req構造体に対象のインターフェース名を設定
i = 0
if_name.to_slice.each do |byte|
  ifreq.ifr_name[i] = byte
  i += 1
end
ifreq.ifr_name[i] = 0

# ioctl関数が負の値を返してきたらエラー
if LibC.ioctl(socket, LibC::SIOCGIFHWADDR, pointerof(ifreq).as(Void*)) < 0
  STDERR << "Error: " << Errno.new(Errno.value) << '\n'
  exit(1)
end

# if_req構造体からMACアドレスを取り出して整形
puts mac = ifreq.ifr_addr.sa_data.to_slice[0, LibC::IFHWADDRLEN].map { |b| b.to_s(16).rjust(2, '0') }.join(':')
$ ip a
      :
      :
2: ens33: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP group default qlen 1000
    link/ether 00:0c:29:44:5b:f7 brd ff:ff:ff:ff:ff:ff
    inet 172.16.239.146/24 brd 172.16.239.255 scope global noprefixroute dynamic ens33
       valid_lft 1646sec preferred_lft 1646sec
    inet6 fe80::c720:1500:965d:9420/64 scope link noprefixroute 
       valid_lft forever preferred_lft forever
$ crystal build get_mac.cr 
$ ./get_mac ens33
00:0c:29:44:5b:f7

やっていることは,LibCにioctl関数関係の定義を追加して,あとは,ガリガリとLibCの機能を叩きまくってる感じです。

Macだとioctl関数がENXIOで落ちるトカ,if_req構造体が簡易版過ぎて他の用途に使えないトカ,アラは色々ありますが,CentOS7環境では一応MACアドレスが取れてきているようです。

低レイヤの機能を触ってみるのも楽しいですね。

時刻と時間

※:この記事はCrystal Advent Calendar 2018 の2日目です。

英語の "time" は 時刻(時の流れのある1点)と時間(2つの時刻の差)の両方の意味を含んでいます。

多くの言語にTimeという名前の型が存在しますが,多くの場合それらが表現しているのは時刻であり,時間は「時刻の差」として表現される場合が多いようです。(少なくとも,私の狭い観測範囲の中においては)

例えば,RubyTimeクラスは時刻を表す型で,独立した時間を表す型は標準では用意されていません。2つのTimeオブジェクトの差をとると,その間の秒数(時間)がFloat型で返されます。

start = Time.now
sleep(5)
finish = Time.now
finish - start
#=> 5.004982

また,Timeオブジェクトに数値(秒数)を加減算することで,ある時刻を基準とした別の時刻を取得することができます。

now = Time.now
#=> 2018-11-30 11:56:54 +0900
now + 3600
#=> 2018-11-30 12:56:54 +0900

本来数値には単位はないため,現在時刻に1を加えた際,それが1秒なのか1時間なのか,はたまた1年なのかは明確ではないのですが,ここでは暗黙的に「時刻に加減算される数値は秒数である」という前提が置かれています。

一方,Crystal では,時刻を表すTime型とは別に,時間を表すTime::Span型が標準で用意されており,コンストラクタを使用する場合は以下のようにして生成することができます。

# 日,時間,分,秒,ナノ秒(省略可)を指定する方式
Time::Span.new(1, 2, 3, 4, 567)
# 1日と2時間3分4秒と567ナノ秒

# 時間,分,秒を指定する方式
Time::Span.new(10, 11, 12)
# 10時間11分12秒

# 名前付き引数で秒,ナノ秒で指定する方式
Time::Span.new(seconds: 128, nanoseconds: 256)
# 128秒と256ナノ秒

# 名前付き引数でナノ秒だけを指定する方式
Time::Span.new(nanoseconds: 4000)
# 4マイクロ秒

Timeオブジェクト同士の差を取ると,Time::Spanオブジェクトが返されますし,

start = Time.now
sleep(5)
finish = Time.now
span = finish - start
#=> 00:00:05.004945000
typeof(span)
#=> Time::Span

Timeオブジェクトに直接数値を加減算することはできず,Time::Spanオブジェクト(もしくは後述するTime::MonthSpanオブジェクト)を使用する必要があります。

p now = Time.now
#=> 2018-11-30 15:10:04.188704000 +09:00 Local

p now + Time::Span.new(1,0,0)
#=> 2018-11-30 16:10:04.188704000 +09:00 Local

p now + 3600
#=> Error: no overload matches 'Time#+' with type Int32

ただ,時間計算の度にいちいちTime::Span.newTime::Spanオブジェクトを生成するのは非常に煩雑ですので,標準の整数型にはTime::Spanオブジェクトを生成するためのインスタンスメソッドが定義されています。

  • 1ナノ秒1.nanosecond
  • 1マイクロ秒:1.microsecond
  • 1ミリ秒:1.millisecond
  • 1秒:1.second
  • 1分:1.minute
  • 1時間:1.hour
  • 1日:1.day
  • 1週間(7日間):1.week

Note: これらのメソッドには複数形(weeksdaysなど)も併せて用意されています。

これらを使用すると,時間操作の可読性を大きく向上させることができます。

# rubyで現在時刻から2週間後の時刻を取得する
Time.now + 3600 * 24 * 14

# crystalで現在時刻から2週間後の時刻を取得する
Time.now + 2.weeks

また,Time::Spanには,自身を各単位(ナノ秒,マイクロ秒,ミリ秒,秒,分,時,日)に換算した数値(Float64型)を返すインスタンスメソッド(total_******に複数形の単位名)が用意されています。ですので,「1週間って何秒だっけ?」という時(そんな状況が実際にあるかどうかは別として)にも以下のようにして求めることができます。

p 1.week.total_seconds
#=> 604800.0

月単位の時間操作

1秒は109ナノ秒で1時間は3600秒,1日は24時間で1週間は7日と,週までの時間単位は(閏秒などの例外をのぞいて)いつを起点としても同じ量になります。しかし,月や年といったそれ以上の時間単位になると,そうもいきません。

「ある時刻の1ヶ月後」を取得したい場合には,その月が何日あるのかを考える必要がでてきます。日付の月要素を取り出して1を加える,という大雑把な方法もありますが,年末を挟むと年も修正する必要があったり,8月31日だと翌月(9月)には31日が存在しなかったりと,こちらも細々とした調整が必要になります。

真面目にやろうとすると意外と煩雑な月単位の時間操作のために,Crystal の標準ライブラリにはTime::Span型とは別に月間を表すTime::MonthSpan型が用意されています。

Time::Span型と同様,Time::MonthSpan型にも整数型に生成用のインスタンスメソッドが用意されていますので,自然な形で月や年単位の時間操作を記述することができます。

  • 1ヶ月間:1.month
  • 1年間:1.year
p Time.new(2018,12,1) + 1.month
#=> 2019-01-01 00:00:00.0 +09:00 Local

この時,例えば「1月30日の1ヶ月後」のように,計算後の月には存在しない日を基準として計算した場合,計算後の日付はその月の末日になります。

p Time.new(2018,10,31) + 1.month
#=> 2018-11-30 00:00:00.0 +09:00 Local

ちゃんと閏年も考慮されており,2019年1月29日の1ヶ月後は2019年2月28日が返されますが,2020年(閏年)の1月29日の1ヶ月後は2020年2月29日が返ってきます。

p Time.new(2019,1,29) + 1.month
#=> 2019-02-28 00:00:00.0 +09:00 Local

p Time.new(2020,1,29) + 1.month
#=> 2020-02-29 00:00:00.0 +09:00 Local

iPadでCrystalコーディング環境

出張の移動中にちょっとしたコーディングがしたくて試行錯誤した内容を備忘録として。

やりたかったこと

導入アプリ1)Textastic

Textastic Code Editor 6

Textastic Code Editor 6

  • Alexander Blach
  • 仕事効率化
  • ¥1,200

iPadで使えるコーディングエディタ。

シンタックスハイライトができて,iCloudドライブやDropboxとも連携できて便利。

当然(?)Crystal言語のハイライトには対応していない……が,Textmateシンタックス定義がそのまま使えるので,GitHubからCrystal.tmbundleを拾ってきて突っ込む。

github.com

突っ込み方は公式で公開されてるiPad版マニュアルのpp. 14-19辺りを参照。

#Textastic フォルダにどうやって.tmbundle 一式を送り込んだら良いのか悩んだものの,GitHubからcloneしたリポジトリ一式を一旦Dropboxにあげて,Textasticのファイル転送機能でフォルダごと持ってきたらできた。

マクロなんかも結構ちゃんと色分けしてくれてニッコリ。

f:id:denchuinc:20180625181942p:plain

シンボルリストからクラス定義やメソッド定義にジャンプしたりもできる。 (注:マクロメソッドの表示が微妙だったり,structがシンボルとして拾われないとかの不具合はある)

f:id:denchuinc:20180625182110p:plain

ただし,Textastic単体ではGitHubからcloneしたりpushしたりはできない。

導入アプリ2) Working Copy

Working Copy

Working Copy

  • Anders Borum
  • 仕事効率化
  • 無料

iPadで使えるGitクライアント。GitHubからローカルへのcloneは無料版でもできる。リモートへのpushは有料機能。

GitHubで認証すると,このアプリ用の公開鍵をGitHubアカウントへ埋め込む。(この挙動自体はどうかと思わないでもないけれど,面白い仕組みではある)

こいつ自身にも簡易のエディタは搭載されているものの,シンタックス定義の追加とかはできない。むしろ,他のテキストエディタから開けるから,エディタは好きなの使って,みたいなスタンスっぽい。

公式サイトの一番上にある動画で17秒あたりから,まさにTextasticでリポジトリを開く方法が紹介されてる。

ローカルにcloneするので,電波が途切れても編集や保存ができる。

その他

コーディング以外でも,iPad標準のVPN機能(L2TP/IPsec/)と

Prompt 2

Prompt 2

  • Panic, Inc.
  • ユーティリティ
  • ¥1,800

辺りのリモートアクセス系アプリを使えば,出先からiPadで最低限の緊急対応はできそうな気配。

今度の出張はMacBook Proを置いて,iPadだけで行ってみようかな。