エンジニアのソフトウェア的愛情

または私は如何にして心配するのを止めてプログラムを・愛する・ようになったか

ビットとバイトとストリングと in Elixir

Elixir でビット操作を行うコードを書いたんですが、使うべき関数やガードで混乱しそうになったので、明日の自分のためにまとめてみました。

ガード

# 入力 is_binary/1 is_bitstring/1
1 "あいう" true true
2 <<12, 34>> true true
3 <<12::5, 34::5>> false true

is_binary/1 はバイト列(ビット長が 8 の倍数になっているビット列)の場合に true になります。 上記の 3 番目は 10 ビットのビット列なので false を返します。

全体で 8 の倍数であれば true になりますので、次のように書いても true になります。

iex> is_binary(<<12::3, 34::5>>)
true

パタンマッチング

パタンマッチングでも binary で受ける場合には、そのビット長が 8 の倍数になっている必要があります。 端数があるとエラーになります。

iex> <<a, rest::binary>> = ""
""

iex> a
227

iex> rest
<<129, 130>>
iex> <<a, rest::binary>> = <<123, 234>>
<<123, 234>>

iex> a
123

iex> rest
<<234>>
iex> <<a, rest::binary>> = <<123::5, 234::5>>
** (MatchError) no match of right hand side value: <<218, 2::size(2)>>

任意の長さのビット列を受けるには bitstring を指定します。

iex> <<a, rest::bitstring>> = ""
""

iex> a
227

iex> rest
<<129, 130>>
iex> <<a, rest::bitstring>> = <<123, 234>>
<<123, 234>>

iex(79)> a
123

iex(80)> rest
<<234>>
iex(81)> <<a, rest::bitstring>> = <<123::5, 234::5>>
<<218, 2::size(2)>>

iex(82)> a
218

iex(83)> rest
<<2::size(2)>>

長さとサイズ

# 入力\関数 String.length/1 byte_size/1 bit_size/1
1 "あいう" 3 9 72
2 <<123, 234>> 2 2 16
3 <<12::5, 34::5>> エラー*1 2 10

*1 ** (FunctionClauseError) no function clause matching in String.Unicode.length/1

String.lenth/1UTF-8 としての文字数を返します。 ビット列の長さが 8 の倍数でない場合、エラーが発生します。

byte_size/1 はバイト数を返しますが、興味深いのはビット列の長さが 8 の倍数でないばあいでもエラーにはならず、8 で割って切り上げた数字を返す点です。

ちなみに lengthsize という名前について。 実行が線形時間のものには length を、定数時間のものには size をつけるようにしてるとのこと。

また、byte_size/1bit_size/1 はガードに記述できるためか、ドキュメントでは Functions でなく Guards に記載されています。

# 内容
1 binary() バイト列(長さが 8 の倍数のビット列)
2 bitstring() ビット列
3 String.t() 文字列(binary()エイリアス
4 string() Erlang の文字列(文字リスト charlist() に同じ)

"string" という単語が 3 回も出てくるので、うろ覚えでいると足をすくわれそうです。

String.t()binary() と同じものをさしていますが、値が文字列(UTF-8 エンコーディングのバイナリ)であることをドキュメントで明確にしたいばあいのために用意されているようです。

また string()Erlang の文字列型であるため、Elixir 内では charlist() を使うように勧められています。

やりたかったこと

「任意のビット列を任意のサイズで分割し、左詰めでバイト列にしたリストを返す」という機能を実現すべく四苦八苦していました。

defmodule Bin do
  @spec chunk_every(bitstring(), pos_integer()) :: [binary()]
  def chunk_every(bin, n) when is_bitstring(bin) and is_integer(n) and n > 0 do
    padding_size = rem(8 - rem(n, 8), 8)

    bin
    |> Stream.unfold(fn
      <<>> ->
        nil

      <<chunk::size(n), rest::bitstring>> ->
        {<<chunk::size(n), 0::size(padding_size)>>, rest}


      chunk ->
        chunk_size = bit_size(chunk)
        padding_size = n - chunk_size + padding_size
        {<<chunk::bitstring, 0::size(padding_size)>>, <<>>}
    end)
    |> Enum.to_list()
  end
end

たとえば。 60 ビットのビット列を 12 ビットごとに分割し、分割したそれぞれのビットを左詰めしたバイト列のリストとして取得したいばあい。

このばあい、12 ビットのビット列は 16 ビット = 2 バイトのバイト列に収まるので、2 バイトのバイト列のリストが返ります。

iex> Bin.chunk_every(<<0x123456789abcdef::60>>, 12)                    
[<<18, 48>>, "E`", <<120, 144>>, <<171, 192>>, <<222, 240>>]

結果がわかりにくいので、パタンマッチングで確認。

iex> Bin.chunk_every(<<0x123456789abcdef::60>>, 12) == [<<0x12, 0x30>>, <<0x45, 0x60>>, <<0x78, 0x90>>, <<0xab, 0xc0>>, <<0xde, 0xf0>>]
true

いつか読むはずっと読まない: ≥ 1.6

第 2 版の邦訳が出版されていることを、つい最近になって知りました。 不覚。 買わねば。

プログラミング Elixir(第2版)

プログラミング Elixir(第2版)

  • 作者:Thomas,Dave
  • 発売日: 2020/12/01
  • メディア: 単行本

ElixirでPNGを出力するための覚書

defmodule PNG do
  @moduledoc """
  PNG イメージを作成します

  cf. https://en.wikipedia.org/wiki/Portable_Network_Graphics
  """

  @magic_number [0x89, "PNG", 0x0D, 0x0A, 0x1A, 0x0A]

  @width 32
  @height 8

  @depth 8
  @color_type 3 # indexed

  @compression_method 0
  @filter_method 0
  @interlace_method 0

  @header <<
    @width::32,
    @height::32,
    @depth,
    @color_type,
    @compression_method,
    @filter_method,
    @interlace_method
  >>

  @palette [
    0x00, 0x00, 0x00, # パレット番号0 - R:0, G:0, B:0
    0xFF, 0xFF, 0xFF  # パレット番号1 - R:255, G:255, B:255
  ]

  @image [
    [ 0, 0, 0, 0,   0, 0, 0, 0,   0, 0, 0, 0,   0, 0, 0, 0,
      1, 0, 0, 1,   0, 0, 0, 0,   0, 0, 0, 0,   0, 0, 0, 0 ],
    [ 0, 1, 1, 0,   0, 1, 1, 0,   1, 0, 0, 0,   1, 1, 0, 0,
      1, 0, 0, 1,   0, 0, 1, 1,   0, 0, 1, 1,   0, 1, 1, 0 ],
    [ 1, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1,
      1, 1, 1, 1,   1, 1, 0, 0,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 1, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 0,
      1, 0, 0, 1,   0, 1, 0, 0,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 1, 1, 1, 1,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 0,
      1, 0, 0, 1,   0, 0, 1, 0,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 1, 0, 0, 0,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 0,
      1, 0, 0, 1,   0, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 1, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1,   0, 1, 0, 0,
      1, 0, 0, 1,   0, 0, 0, 1,   0, 1, 0, 1,   0, 1, 0, 1 ],
    [ 0, 1, 1, 0,   0, 1, 0, 1,   0, 1, 0, 0,   1, 0, 1, 0,
      0, 1, 0, 0,   1, 1, 1, 0,   0, 0, 1, 0,   1, 1, 0, 1 ]
  ]

  @scanline_filter_none 0

  def write_image(filename) do
    rows =
      Enum.map(@image, fn row ->
        [@scanline_filter_none, row]
      end)

    File.open(filename, [:write], fn io ->
      IO.binwrite(io, @magic_number)
      write_chunk(io, "IHDR", @header)
      write_chunk(io, "PLTE", @palette)
      write_chunk(io, "IDAT", compress(rows))
      write_chunk(io, "IEND", "")
    end)
  end

  def write_chunk(io, type, data) do
    length = :erlang.iolist_size(data)
    crc = :erlang.crc32([type, data])

    IO.binwrite(io, <<length::big-size(32)>>)
    IO.binwrite(io, type)
    IO.binwrite(io, data)
    IO.binwrite(io, <<crc::big-size(32)>>)
  end

  @doc """
  Erlang の zlib モジュールを利用してデータを圧縮します

  see http://erlang.org/doc/man/zlib.html
  """
  def compress(data) do
    zip = :zlib.open()
    :ok = :zlib.deflateInit(zip)
    compressed = :zlib.deflate(zip, data, :finish)
    :ok = :zlib.deflateEnd(zip)
    :ok = :zlib.close(zip)

    compressed
  end
end
$ elixirc png.ex
$ elixir -e 'PNG.write_image("emattsan.png")'

ElixirからErlangのASN.1のライブラリを利用するための覚書

Erlang には、 ASN.1 で記述されたデータ構造のコンパイルと、エンコード/レコードをおこなうライブラリが標準で用意されています。

erlang.org

ja.wikipedia.org

かつて通信機器の開発が仕事だったころにあつかっていたことを思い出しつつ、Elixir で利用する方法を記録しておきます。

とはいえ。 ASN.1 については Erlangチュートリアルそのままで、生成した Erlang のモジュールをどのように Elixir から利用するか、というお話がすべてです。

Erlang で ASN.1 のモジュールを生成する

Erlang のマニュアルの Getting Started にしたがって、モジュールを作成します。

まず、定義ファイル People.asn を作成します。

People DEFINITIONS AUTOMATIC TAGS ::=
BEGIN
  Person ::= SEQUENCE {
    name PrintableString,
    location INTEGER {home(0),field(1),roving(2)},
    age INTEGER OPTIONAL
  }
END

次にこれをコンパイルしてモジュールを生成します。 Elixir から Erlang のモジュールや関数を利用するばあいは、特に引数の記述の違いに注意してください。

iex> :asn1ct.compile('People', [:per])
:ok

コンパイルに成功すると、次のファイルが生成されます。

People.asn1db
People.beam
People.erl
People.hrl

生成されたモジュールの関数を Elixir から呼び出す

コンパイルを実行した iex から、あるいは生成されたファイルがあるディレクトリで iex を起動してそこから、次の関数を実行します。

iex> {:ok, bin} = :People.encode(:Person, {:Person, 'Some Name', :roving, 50})
{:ok, <<128, 9, 83, 111, 109, 101, 32, 78, 97, 109, 101, 1, 2, 1, 50>>}

ここで Erlang のモジュール名が People であるため、Elixir からは :People という名前で参照することに注意してください。

続いてエンコードした結果をデコードしてみます。

iex> :People.decode(:Person, bin)
{:ok, {:Person, 'Some Name', :roving, 50}}

元のデータに復元できることが確認できました。

生成されたモジュールを Elixir のプロジェクトに含める

生成した Erlang のモジュールを mix new コマンドで作成する Elixir のプロジェクトで利用できるようにします。

Elixir のプロジェクトでは、コンパイル対象にする Erlang のソースファイルの格納パスが src/ に設定されています。 このため作成したプロジェクト内に src/ というディレクトリを作成し、そこに先に生成された Erlang のソースファイルとヘッダファイルをコピーするだけでモジュールを利用できるようになります。

my_app/
└── src/
    ├── People.erl
    └── People.hrl

コンパイル対象にするパスは、 mix.exsproject/0:erlc_paths を設定することで変更することができます。 くわしくは Mix.Project — Mix v1.11.3mix compile.erlang — Mix v1.11.3 を参照してみてください。

ファイルのコピーができたら、エンコード/デコードができることを確認してみます。

my_app $ iex -S mix

iex(1)> {:ok, bin} = :People.encode(:Person, {:Person, 'Some Name', :roving, 50})
{:ok, <<128, 9, 83, 111, 109, 101, 32, 78, 97, 109, 101, 1, 2, 1, 50>>}

iex(2)> {:ok, result} = :People.decode(:Person, bin)
{:ok, {:Person, 'Some Name', :roving, 50}}

Rustlerを使ってElixirのNIFを書たときに異なる型の要素を含むリストをあつかいたいばあいの覚書

結論: Vec<Term<'a>> 型にデコードするとよい

リスト全体は Vec<T> 型で受けるとして、T になにを書けばよいのかしばし悩んだのですが、Erlant Term のままあつかうのでよければ Term<'a> 型で受ければよいということに気がつきました。

fn count<'a>(env: Env<'a>, args: &[Term<'a>]) -> Result<Term<'a>, Error> {
    let list: Vec<Term<'a>> = args[0].decode()?;

    Ok(list.len().encode(env))
}

各要素の具体的な値を利用したいばあいは、おのおの必要になった時点でデコードすればよさそうです。

実例

結論を書いてしまったので、以下は Rustler を使う手順の一般的な覚書です。

Rustler を導入する

Rustler は Rust で NIF を書くための便利なライブラリです。 上に書いたコードのように、とても簡単に Rust と Elixir をインタフェースしてくれます。

hex.pm

プロジェクトを作成し、依存するパッケージに Rustler を追加します。

$ mix new my_app
$ cd my_app
  defp deps do
    [
      {:rustler, "~> 0.21"}
    ]
  end

パッケージを導入すると mix rustler.new コマンドが利用できるようになります。

$ mix do deps.get, deps.compile
$ mix help
...
mix rustler.new         # Creates a new Rustler project
...

NIF の雛形を生成する

追加された mix rustler.new コマンドを使っって NIF の雛形を生成します。

モジュール名を訊かれるので入力します。

続けてライブラリ名を訊かれます。 モジュール名から生成した名前が示されるので、その名前でよければそのまま決定します。

native という名前のディレクトリの下に指定したライブラリ名でパッケージが作成されます。

$ mix rustler.new
This is the name of the Elixir module the NIF module will be registered to.
Module name > MyApp.List
This is the name used for the generated Rust crate. The default is most likely fine.
Library name (myapp_list) >
* creating native/myapp_list/.cargo/config
* creating native/myapp_list/README.md
* creating native/myapp_list/Cargo.toml
* creating native/myapp_list/src/lib.rs

Rust のコードを編集する

native/myapp_list/src/lib.rs を編集します。 ここではリストを受け取って要素の数を返す関数を書きます。

要素の数だけを知りたいので、今回は Erlang Term のままであつかい個々の要素はデコードしません。 このため先に書いたとおり、受け取った引数のリストを Vec<Term<'a>> という型にデコードします。

use rustler::{Encoder, Env, Error, Term};

fn count<'a>(env: Env<'a>, args: &[Term<'a>]) -> Result<Term<'a>, Error> {
    let list: Vec<Term<'a>> = args[0].decode()?;

    Ok(list.len().encode(env))
}

rustler::rustler_export_nifs! {
    "Elixir.MyApp.List",
    [
        ("count", 1, count)
    ],
    None
}

NIF をマウントする Elixir のモジュールを作成する

Rust のコードに書かれたモジュール名の Elixir のモジュールを作成します。

モジュールでは Rustler を use し、オプションで OTP アプリケーション名と、Rust のライブラリ名を指定します。

defmodule MyApp.List do
  use Rustler, otp_app: :my_app, crate: :myapp_list

  def count(_list), do: :erlang.nif_error(:nif_not_loaded)
end

コンパイラとパッケージを指定する

mix.exs を編集してコンパイラに Rustler を追加し、利用するパッケージの指定を追加します。

  def project do
    [
      app: :my_app,
      version: "0.1.0",
      elixir: "~> 1.11",
      start_permanent: Mix.env() == :prod,
      compilers: [:rustler | Mix.compilers()], # 追加 - コンパイラに Rustler を追加する
      rustler_crates: [myapp_list: []],        # 追加 - myapp_list を利用するパッケージに追加する
      deps: deps()
    ]
  end

実行

実行します。 コンパイラに Rustler を追加しているので、Elixir のコードがコンパイルされるのと一緒に Rust のパッケージもコンパイルされます。

$ mix run -e 'MyApp.List.count([1, :ok, {}, [], "ABC"]) |> IO.inspect()'
Compiling NIF crate :myapp_list (native/myapp_list)...
    Finished release [optimized] target(s) in 0.19s
Compiling 2 files (.ex)
Generated my_app app
5

いつか読むはずっと読まない:ラスト・モンスターに泣かされるレベル1パーティー

なけなしのお金で購入した金属製のアイテムをことごとく錆びつかせるラスト・モンスターが、キャリオン・クロウラーと並んで低レベルパーティーの憎き敵役だったのもよい思い出。

ダンジョンズ&ドラゴンズ スターター・セット第5版

ダンジョンズ&ドラゴンズ スターター・セット第5版

  • 発売日: 2017/12/18
  • メディア: おもちゃ&ホビー

実践Rust入門[言語仕様から開発手法まで]

実践Rust入門[言語仕様から開発手法まで]

Phoenix LiveView で temporary_assigns を設定したときにどのように DOM を削除するか

動作確認に利用したコードは GitHub に push しました。 この記事で解説しているコードは このコミット にまとまっています。

temporary_assigns を設定したときの削除問題

LiveView では、一部が更新されたときに全体を再描画するのではなく、更新された部分だけを再描画するように設定することができます。

hexdocs.pm

ただし可能なのは、新規の DOM の追加と既存の DOM の更新だけで、削除はできません。 削除するには、該当する DOM を削除した全体をサーバからクライアントに送り、再描画する必要があります。 要素の数が少なければそのような方法も使えますが、数が多くなると送信のコストも再描画のコストも無視できなくなります。

これは Elixir Forum でも質問にあがっていた内容です。

elixirforum.com

解決策としては、Forum のこの質問にも回答されているように、JavaScript のフックを使って削除されたことの情報を持っている DOM を削除するというのが現実的なようです。

hexdocs.pm

と、いうわけで。 具体的に実装してみました。

実装のアイディア

アイディアはいたって単純です。

削除したい要素の DOM を更新して、削除の情報を設定します。 DOM の更新後に呼び出されるフック関数で、削除の情報が設定された DOM を削除します。

前提として。

ここでは Ecto ででデータベースを利用していることを想定しています。 ですが、対応するデータが削除されたことが DOM に反映されるようになっていれば、Ecto でなくてもデータベースのデータでなくても、同じように処理できます。

Ecto.Schema の状態を調べる

Ecto.Schema には :__meta__ という名前でメタデータが格納され、そこには :state という名前で状態が格納されています。

hexdocs.pm hexdocs.pm

メタデータの値を取得する Ecto.get_meta/2 が用意されているので、これで状態を確認できます。

Hoge というプロジェクトの Foos というコンテクストに Bar というスキーマが設定され bars というテーブルがあるような状況で、状態を確認してみます。

iex(1)> {:ok, bar} = Hoge.Foos.create_bar(%{name: "hoge"})
{:ok,
 %Hoge.Foos.Bar{
   __meta__: #Ecto.Schema.Metadata<:loaded, "bars">,
   id: 1,
   inserted_at: ~N[2020-12-30 23:23:13],
   name: "hoge",
   updated_at: ~N[2020-12-30 23:23:13]
 }}

iex(2)> Ecto.get_meta(bar, :state)
:loaded
iex(3)> {:ok, bar} = Hoge.Foos.get_bar!(1) |> Hoge.Foos.delete_bar()
{:ok,
 %Hoge.Foos.Bar{
   __meta__: #Ecto.Schema.Metadata<:deleted, "bars">,
   id: 1,
   inserted_at: ~N[2020-12-30 23:23:13],
   name: "hoge",
   updated_at: ~N[2020-12-30 23:23:13]
 }}

iex(4)> Ecto.get_meta(bar, :state)                                  
:deleted

state の値は次の 3 種類です。

状態
:built データはメモリ上に構築されているがデータベースに保存されていない状態
:loaded データはデータベースに保存されている状態
:deleted データはデータベースから削除されている状態

DOM にスキーマの状態を持たせる

これを利用して。 例えば data-state という属性を追加して、DOM にスキーマの状態を追加します。 また phx-hook でフックを設定します。

<div id="bars" phx-update="append">
  <%= for bar <- @bars do %>
    <div id="bar-<%= bar.id %>" data-state="<%= Ecto.get_meta(bar, :state) %>" phx-hook="Bar">
      <span><%= bar.id %></span>: <span><%= bar.name %></span>
    </div>
  <% end %>
</div>

JavaScript のフックで削除された状態の DOM を削除する

フックでは、DOM が更新された後に呼び出される関数 updated の中で data-state の内容を調べて、"deleted" になっていれば、状態が更新された要素を削除します。

let Hooks = {}

Hooks.Bar = {
  updated() {
    if (this.el.dataset.state == "deleted") {
      this.el.remove()
    }
  }
}

let liveSocket = new LiveSocket("/live", Socket, {hooks: Hooks, params: {_csrf_token: csrfToken}})

Hotwire のことを少し

先日 Hotwire が公表されました。 サーバで DOM を構築して送り出すことでページを更新するという、LiveView とも似ている部分があります。

さっそくさわってみました。

気がついたことはいくつかあるのですが、最初に目がいったのは部分的な DOM の削除の箇所。 予想どおりですが、削除のアクションが用意されていて、メソッド呼び出しひとつで指定した DOM を削除できるようになっていました。

www.rubydoc.info

実は LiveView での部分的な DOM の削除は、以前から気になっていた箇所です。 気になりつつも先送りしていたのですが、今回 Hotwire でそれができると知り、がぜん解決したくなったというのが今回の記事を書く動機になりました。

仕事では Ruby on Rails を主戦場にしていることもあり、ゆくゆくは Hotwire を使う機会があるかもしれません。 JavaScript 嫌いマン、JavaScript 書きたくないマン であるわたしにとっては LiveView や Hotwire のようなフレームワークが普及することを願って止みません。

Hotwire については。もう少し使ってみてから記事にしようと思います。

いつか読むはずっと読まない:YOUはSHOCK

日々仕事でデータベースを扱っていて。 SQL以外にもデータをうまくあつかえる手段を手に入れたい、という思いが高まり。 一度は R に手を出したものの挫折。

構文が肌に合わなかったのかもしれないと、Julia に手を出し。 現在挫折中。

構文云々ではなく、まずなにより「データをあつかう」という領域のメンタルモデルが必要なのかもしれない、と感じる今日このごろ。

1から始める Juliaプログラミング

1から始める Juliaプログラミング

そんなわけで。 みなさん、よいお年を。

覚書 join.h

#ifndef __JOIN_H__
#define __JOIN_H__

#include <sstream>
#include <iterator>

template<class Iterator>
std::string join(Iterator begin, Iterator end) {
    std::ostringstream io;

    for(Iterator i = begin; i != end; ++i) {
        io << *i;
    }

    return io.str();
}

template<class Iterator, class Sep>
std::string join(Iterator i, Iterator end, const Sep& sep) {
    std::ostringstream io;

    io << *i;

    while(++i != end) {
        io << sep << *i;
    }

    return io.str();
}

#endif// __JOIN_H__

C++のクラスをNIFでElixirにバインドしてみた

C++ のライブラリを Elixir から利用したくなりました。

NIF を使った単純な関数の呼び出しは実装したことがあるのですが、オブジェクトを利用するようなケースは試したことがなかったので、今回調べてみました。

今回のコードではエラーハンドリングなどは省略していますのでご注意ください。 不備がありましたらご指摘いただけるとありがたいです。

またここで実装したファイルは Gist にもアップしていますので合わせてご参照ください。

NIF環境の用意

elixir_make

mix compile コマンドで C++ のコードがいっしょにコンパイルされるように、elixir_make を導入します。

hex.pm

まず依存パッケージとして mix.exselixir_make を追加します。 このパッケージはコンパイル時のみ利用するので [runtime: false] のオプションを指定します。

またコンパイラのリストに :elixir_make を追加して、 mix compile 実行時に合わせて実行されるようにします。

defmodule Counter.MixProject do
  use Mix.Project

  def project do
    [
      app: :counter,
      version: "0.1.0",
      elixir: "~> 1.11",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      compilers: [:elixir_make | Mix.compilers()] # 追加
    ]
  end

  def application do
    [
      extra_applications: [:logger]
    ]
  end

  defp deps do
    [
      {:elixir_make, "~> 0.6", runtime: false} # 追加
    ]
  end
end

counter_nif

NIF のコードを C++ で記述します。 ここではまず動作確認のために引数の値をそのまま戻り値として返す identity/1 を実装してみます。

// c_src/counter_nif.cpp

#include <erl_nif.h>

static ERL_NIF_TERM identity(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    return argv[0];
}

static ErlNifFunc nif_funcs[] = {
    {"identity", 1, identity}
};

ERL_NIF_INIT(Elixir.Counter, nif_funcs, nullptr, nullptr, nullptr, nullptr);

Makefile

Makefile を用意します。

変数の意味の詳細については elixir_makeのドキュメント を参照してください。

PREFIX = $(MIX_APP_PATH)/priv
BUILD = $(MIX_APP_PATH)/obj

NIF = $(PREFIX)/counter_nif.so

CFLAGS = -std=c++11 -fpic
LDFLAGS = -lpthread -dynamiclib -undefined dynamic_lookup

ERL_CFLAGS = -I$(ERL_EI_INCLUDE_DIR)
ERL_LDFLAGS = -L$(ERL_EI_LIBDIR) -lei

SRC = $(wildcard c_src/*.cpp)
HEADERS = $(wildcard c_src/*.h)
OBJ = $(SRC:c_src/%.cpp=$(BUILD)/%.o)

all: install

install: $(PREFIX) $(BUILD) $(NIF)

$(OBJ): $(HEADERS) Makefile

$(BUILD)/%.o: c_src/%.cpp
  $(CC) -c $(CFLAGS) $(ERL_CFLAGS) -o $@ $<

$(NIF): $(OBJ)
  $(CC) $(LDFLAGS) $(ERL_LDFLAGS) -o $@ $^

$(PREFIX) $(BUILD):
  mkdir -p $@

clean:
  $(RM) $(NIF) $(OBJ)

.PHONY: all clean install

Counter module

NIF をバインドする Elixir のモジュールを記述します。

defmodule Counter do
  @on_load {:load_nif, 0}
  @compile {:autoload, false}

  def load_nif do
    Application.app_dir(:counter, "priv/counter_nif")
    |> to_charlist()
    |> :erlang.load_nif(0)
  end

  def identity(_term), do: :erlang.nif_error(:nif_not_loaded)
end

動作確認

ここまでの編集でファイル構成はこのようになっています。

├── Makefile
├── README.md
├── c_src/
│   └── counter_nif.cpp
├── lib/
│   └── counter.ex
├── mix.exs
└── test/
    ├── counter_test.exs
    └── test_helper.exs

mix deps.get でパッケージを取得し、 iex -S mix で IEx を起動します。

問題がなければ make が実行され、Makefile に記述した内容で NIF が作成されます。 Counter.identiy/1 を呼び出して NIF が機能していることを確認します。

$ mix deps.get
$ iex -S mix
iex(1)> Counter.identity(123)
123
iex(2)> Counter.identity("abc")
"abc"
iex(3)> Counter.identity(:abc) 
:abc

C++の Counter クラス

C++ のクラスを追加します。

ここでは簡単なカウンタクラスを例として用います。

// c_src/counter.h

#ifndef COUNTER_H_
#define COUNTER_H_

class Counter {
public:
    Counter(int count = 0) : count_(count) {}
    ~Counter() {}

    int up() { return ++count_; }
    int down() { return --count_; }
    void set(int count) { count_ = count; }
    int get() const { return count_; }

private:
    int count_;
};

#endif//COUNTER_H_

リソースの確保と解放

リソースの確保では次の関数を利用します。

また enif_open_resource_type は NIF がロードされたときに呼び出される必要があるので、ロード時に呼び出されるコールバック関数を用意します。

リソースの解放は garbage collection にまかせます。 リソースが解放されるときに呼び出される関数を用意し、 enif_open_resource_type でその関数を設定します。

counter_nif

c_src/counter_nif.cpp を次のように変更します。 ここではリソースを確保されたときと解放されたときの様子がわかるように enif_fprintf を挿入しています。

#include <cstdio>
#include <erl_nif.h>

// 先に定義した Counter クラスの定義ファイル
#include "counter.h"

// リソースタイプを保存する変数
ErlNifResourceType* CounterType;

// リソースが破棄されるときに呼び出される関数
void destruct_counter(ErlNifEnv* caller_env, void* obj) {
    // リソースが解放される前に、必要な破棄の操作をここに記述する
    enif_fprintf(stdout, "destruct %p\n", obj);
}

// NIF がロードされたときに呼び出される関数
int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info) {
    // リソースタイプを作成する
    // 第4引数にリソースが破棄されるときに呼び出される関数を指定する
    CounterType = enif_open_resource_type(caller_env, "Counter", "Counter", destruct_counter, ERL_NIF_RT_CREATE, nullptr);

    return 0;
}

// リソースを確保する関数。Elixir から呼び出される
ERL_NIF_TERM create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    // リソースを確保する
    void* resource = enif_alloc_resource(CounterType, sizeof(Counter));

    // 確保されたリソースに対して、必要な構築の操作をここに記述する
    enif_fprintf(stdout, "construct %p\n", resource);

    // Erlang がリソースを管理するためのハンドルを作成する
    // これで garbage collection の対象になる
    ERL_NIF_TERM handle = enif_make_resource(env, resource);

    // リソースへの参照を手放す
    enif_release_resource(resource);

    // ハンドルを返却する
    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        handle
    );
}

static ErlNifFunc nif_funcs[] = {
    {"create", 1, create}
};

// 第3引数にロード時に呼び出される関数を指定する
ERL_NIF_INIT(Elixir.Counter, nif_funcs, load, nullptr, nullptr, nullptr);

Counter module

Counter を次のように変更します。

defmodule Counter do
  @on_load {:load_nif, 0}
  @compile {:autoload, false}

  def load_nif do
    Application.app_dir(:counter, "priv/counter_nif")
    |> to_charlist()
    |> :erlang.load_nif(0)
  end

  def create(_init_count), do: :erlang.nif_error(:nif_not_loaded)
end

動作確認

IEx から Counter.create/1 を呼び出します。

$ iex -S mix
iex(1)> Counter.create(0)     
construct 0x00007ff929604a78
{:ok, #Reference<0.3631837740.152436741.82065>}

NIF 内に記述したコードによってリソースのポインタが表示されることが確認できます。 またリソースのハンドルは Elixir の環境からは reference として参照されることがわかります。

このリソースは garbage collection によって解放されますが、上記のように実行すると IEx のシェルの環境から参照されているため解放を確認できません。

mix run コマンドで実行して解放を確認します。

$ mix run -e 'Counter.create(0)'
construct 0x00007fb214e033e8
destruct 0x00007fb214e033e8

リソース解放時に呼び出される関数として登録した destruct_counter が実際に呼び出されていることがわかります。

オブジェクトの構築と破棄

メモリの獲得と解放/分離とオブジェクトの構築/破棄を分離して行うための構文、 placement new と placement delete を利用します。

c_src/counter_nif.cpp を次のように変更します。

#include <new>
#include <erl_nif.h>

#include "counter.h"

ErlNifResourceType* CounterType;

void destruct_counter(ErlNifEnv* caller_env, void* obj) {
    Counter* counter = static_cast<Counter*>(obj);

    // placement delete でオブジェクトを破棄する
    counter->~Counter();
}

int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info) {
    CounterType = enif_open_resource_type(caller_env, "Counter", "Counter", destruct_counter, ERL_NIF_RT_CREATE, nullptr);

    return 0;
}

ERL_NIF_TERM create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    int count;
    enif_get_int(env, argv[0], &count);

    void* resource = enif_alloc_resource(CounterType, sizeof(Counter));

    // placement new でオブジェクトを構築する
    new(resource) Counter(count);

    ERL_NIF_TERM handle = enif_make_resource(env, resource);
    enif_release_resource(resource);

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        handle
    );
}

static ErlNifFunc nif_funcs[] = {
    {"create", 1, create}
};

ERL_NIF_INIT(Elixir.Counter, nif_funcs, load, nullptr, nullptr, nullptr);

メンバ関数の呼び出し

あとは構築したオブジェクトに対して操作を行う関数を追加するだけです。

Elixir のコードに渡された reference = リソースのハンドルを再び NIF に返すと、enif_get_resource でリソースのポインタとして受け取ることができます。

    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

もしくは

    Counter* counter;
    enif_get_resource(env, argv[0], CounterType, reinterpret_cast<void**>(&counter));

counter_nif

Counterメンバ関数 up, down, set, get に対応する関数を実装します。

c_src/counter_nif.cpp を次のように変更します。

#include <new>
#include <erl_nif.h>

#include "counter.h"

ErlNifResourceType* CounterType;

void destruct_counter(ErlNifEnv* caller_env, void* obj) {
    Counter* counter = static_cast<Counter*>(obj);
    counter->~Counter();
}

int load(ErlNifEnv* caller_env, void** priv_data, ERL_NIF_TERM load_info) {
    CounterType = enif_open_resource_type(caller_env, "Counter", "Counter", destruct_counter, ERL_NIF_RT_CREATE, nullptr);

    return 0;
}

ERL_NIF_TERM create(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    int count;
    enif_get_int(env, argv[0], &count);

    void* resource = enif_alloc_resource(CounterType, sizeof(Counter));
    new(resource) Counter(count);

    ERL_NIF_TERM handle = enif_make_resource(env, resource);
    enif_release_resource(resource);

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        handle
    );
}

ERL_NIF_TERM up(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

    int result = counter->up();

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        enif_make_int(env, result)
    );
}

ERL_NIF_TERM down(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

    int result = counter->down();

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        enif_make_int(env, result)
    );
}

ERL_NIF_TERM set(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

    int count;
    enif_get_int(env, argv[1], &count);

    counter->set(count);

    return enif_make_atom(env, "ok");
}

ERL_NIF_TERM get(ErlNifEnv* env, int argc, const ERL_NIF_TERM argv[]) {
    void* resource;
    enif_get_resource(env, argv[0], CounterType, &resource);
    Counter* counter = static_cast<Counter*>(resource);

    int result = counter->get();

    return enif_make_tuple2(
        env,
        enif_make_atom(env, "ok"),
        enif_make_int(env, result)
    );
}

static ErlNifFunc nif_funcs[] = {
    {"create", 1, create},
    {"up", 1, up},
    {"down", 1, down},
    {"set", 2, set},
    {"get", 1, get}
};

ERL_NIF_INIT(Elixir.Counter, nif_funcs, load, nullptr, nullptr, nullptr);

Counter module

Counter にも対応する関数を定義します。

defmodule Counter do
  @on_load {:load_nif, 0}
  @compile {:autoload, false}

  def load_nif do
    Application.app_dir(:counter, "priv/counter_nif")
    |> to_charlist()
    |> :erlang.load_nif(0)
  end

  def create(_init_count), do: :erlang.nif_error(:nif_not_loaded)
  def up(_ref), do: :erlang.nif_error(:nif_not_loaded)
  def down(_ref), do: :erlang.nif_error(:nif_not_loaded)
  def set(_ref, _count), do: :erlang.nif_error(:nif_not_loaded)
  def get(_ref), do: :erlang.nif_error(:nif_not_loaded)
end

動作確認

counter $ iex -S mix
iex(1)> {:ok, ref} = Counter.create(123)
{:ok, #Reference<0.2550255970.1501691911.208805>}
iex(2)> Counter.get(ref)
{:ok, 123}
iex(3)> Counter.up(ref); Counter.up(ref); Counter.up(ref)
{:ok, 126}
iex(4)> Counter.get(ref)                                 
{:ok, 126}
iex(5)> Counter.down(ref)                                
{:ok, 125}
iex(6)> Counter.get(ref) 
{:ok, 125}
iex(7)> Counter.set(ref, 321) 
:ok
iex(8)> Counter.get(ref)     
{:ok, 321}

いつか読むはずっと読まない:本を読む

電車での移動中によく本を読むのですが、最近は外出する機会がめっきり減ってしまい、それにともなって本を読む時間も減ってしまいまいました。 外出しないだけ自宅での時間が取れるのだから、その分だけ本を読めるはずなのですが、なかなか思うようにいきません。