20201204のMacに関する記事は4件です。

Big Surアップデート後に特定のコマンドがkillされる

本当にしょうもない記事なのですが自分は1時間かかってしまっているので記録します。
同じ問題に直面した人の助けになれば幸いです。

環境

iMac 2017
Mac OS 11.01
素人zsh環境

起こった問題

OSX HighSierraからのアップデート後、zshの補完が上手くいかないことに疑問を覚えました。
brewのアップデートをしていないなと思いupgrade, updateどちらを試すもエラーが出ました。
スクリーンショット 2020-12-04 21.25.11.png
pipコマンドやbrew、dockerなどの特定のコマンドが瞬時に強制終了されてしまう・・・
(python、vim、自作コマンドなんかは動く)
brewが悪そうだとアタリをつけてGit must be ...で検索をかけると以下の解決方法が載っていました。

$ sudo rm -rf /Library/Developer/CommandLineTools
$ xcode-select --install  

コマンドラインツールのバージョンが古い説、とのことです。
指示に従った後、一旦ターミナルを再起動させるも解決せず...
常に実行しているプロセスがあるためためらっていた本体の再起動をしました。

・・・
−>解決しました

解決法

再起動しないといけないです。なんでもそうですよね。

正直申しますとなぜ解決したのかがわかっていません。Xcodeのバージョンなのか、それとも他のところで詰まっていて再起動が解決したのか、両方なのか...
後学のためにご教授くださると大変嬉しく思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Elixir から Swift 5.3のコードを呼び出す方法(Autotoolsを使って / Apple Silicon M1チップにも対応)

この記事はElixir Advent Calendar 2020の10日目です。

昨日は @kobae964 さんの「Rustler で行儀の良い NIF を書く」でした。

さて,Apple Silicon M1チップの性能が明らかになるにつれて,今後,ElixirエコシステムからMacの潜在能力をフル活用することが求められてくるように思います(というか,是非活用したい)。そこで,この記事はElixirからSwift 5.3のコードを呼び出す方法について紹介しています。

Autotoolsを使ってObjective-CやSwiftのヘッダファイルや関数の存在判定などもできるようにする方法も示していますので,Elixir開発者だけでなく,iOSネイティブアプリやMacアプリの開発者にも部分的には有用かと思います。しかも,Apple Silicon M1チップ搭載のMacでも動作検証をしています!

この記事のGitHubレポジトリは https://github.com/zacky1972/swift_elixir_test です。下記に同様のもののつくりかたを詳説しています。

注意点: この記事はApple Silicon M1チップ搭載のMacに概ね対応していますが,(1)ErlangをARMネイティブでビルドして(2)かつRosetta 2モードでターミナルを起動した場合には,NIFのロード時にアーキテクチャの不一致によるエラーが発生し,NIFを実行できないという問題があります。この問題は,従来のNIFプログラム全般で発生する可能性のある問題だと認識しています。現在,さらに調査を進めて問題の解決に当たっているところです。

まずは mix new

まずはmix newでプロジェクトを作ります。プロジェクト名はswift_elixir_testとしました。

% mix new swift_elixir_test
* creating README.md
* creating .formatter.exs
* creating .gitignore
* creating mix.exs
* creating lib
* creating lib/swift_elixir_test.ex
* creating test
* creating test/test_helper.exs
* creating test/swift_elixir_test_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd swift_elixir_test
    mix test

Run "mix help" for more commands.
% 

書かれている指示に従って,進めます。

% cd swift_elixir_test
swift_elixir_test % mix test
Compiling 1 file (.ex)
Generated swift_elixir_test app
..

Finished in 0.03 seconds
1 doctest, 1 test, 0 failures

Randomized with seed 231147
swift_elixir_test % 

ここで自動生成されたコードを修正しておきます。

lib/swift_elixir_test.ex
defmodule SwiftElixirTest do
  @moduledoc """
  Documentation for `SwiftElixirTest`.
  """

  @doc """
  Hello world.

  ## Examples

      iex> SwiftElixirTest.hello()
      :world

  """
  def hello do
    :world
  end
end

これを次のようにします。(hello関数を削除)

lib/swift_elixir_test.ex
defmodule SwiftElixirTest do
  @moduledoc """
  Documentation for `SwiftElixirTest`.
  """
end

そして自動生成されたテストコードも修正します。

test/swift_elixir_test_test.exs
defmodule SwiftElixirTestTest do
  use ExUnit.Case
  doctest SwiftElixirTest

  test "greets the world" do
    assert SwiftElixirTest.hello() == :world
  end
end

これを次のようにします。

test/swift_elixir_test_test.exs
defmodule SwiftElixirTestTest do
  use ExUnit.Case
  doctest SwiftElixirTest
end

ここで,mix testを実行して,テストが0個になることを確認します。

swift_elixir_test % mix test
Compiling 1 file (.ex)


Finished in 0.02 seconds
0 failures

Randomized with seed 71756
swift_elixir_test % 

ここまでできたら,gitに登録しましょう。

swift_elixir_test % git init
swift_elixir_test % git add -A
swift_elixir_test % git commit -m "initial commit"
swift_elixir_test % git branch -M main

Autoconf の初期設定

ここではAutoconfを使ってビルド時の環境を認識するようにします。ただしAutoconfで生成した環境認識スクリプトconfigureは並列ビルドできないという欠点があるため遅いという難点があります。せっかく並列実行に強いElixirなので,将来はElixirで並列実行できるようにしたいですが,将来課題とします。

まず空のconfigure.acを作成します。

configure.ac
dnl Process this file with autoconf to produce a configure script

AC_INIT()
  • dnlで始まる行はコメント行です。
  • AC_INIT()autoconfに初期化を指示します。パラメータを与えるのが普通なのですが,いったん無しで実行します。

この状態でautoconfを実行します。もしHomebrewを使っているならあらかじめ次のコマンドを実行しておきます。

swift_elixir_test % brew install autoconf

ではautoconfを実行しましょう。

swift_elixir_test % autoconf

そうすると次のファイルが生成されます。

autom4te.cache configure

.gitignoreに下記を追記してgitが追加ファイルを無視するようにしましょう。

.gitignore
# For Autoconf
/autom4te.cache/

# For configure
/configure

configureを実行してみます。

swift_elixir_test % ./configure

するとconfig.logが生成されるので,これも.gitignoreに下記を追記して無視するように設定します。

.gitignore
# For configure
/config.log
/configure

elixir_makeconfigureを呼ぶ

elixir_makeを使うとmix compileをしたときにmakeを用いたビルドをしてくれます。Elixirの作者のJosé Valim(ジョゼ・ヴァリム)にelixir_makeを使ってconfigureを呼び出す方法を教えてもらいました( https://github.com/elixir-lang/elixir_make/issues/42 )ので,紹介したいと思います。

まず,mix.exsを書き換えてelixir_makeをインストールします。mix.exsの下記の部分がインストールするライブラリを指定する部分です。

mix.exs
  defp deps do
    [
      # {:dep_from_hexpm, "~> 0.3.0"},
      # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
    ]
  end

これを次のように書き換えます。

mix.exs
  defp deps do
    [
      {:elixir_make, "~> 0.6.2", runtime: false}
    ]
  end

それから次のコマンドを実行します。

swift_elixir_test % mix deps.get

これでelixir_makeがインストールされました。

次にmix.exsに次のような関数を追加します。System.cmd("#{File.cwd!()}/configure", []) で./configure` を実行することになります。

mix.exs
  defp configure(_args) do
    System.cmd("#{File.cwd!()}/configure", [])
  end

そしてmix.exsの下記の部分がプロジェクト情報なのですが,これを書き換えます。

mix.exs
  def project do
    [
      app: :swift_elixir_test,
      version: "0.1.0",
      elixir: "~> 1.11",
      start_permanent: Mix.env() == :prod,
      deps: deps()
    ]
  end

次のようにします。

mix.exs
  def project do
    [
      app: :swift_elixir_test,
      version: "0.1.0",
      elixir: "~> 1.11",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      compilers: [:elixir_make] ++ Mix.compilers,
      aliases: [compile: [&configure/1]]
    ]
  end

このようにすると,makeを呼び出す代わりに./configureを呼び出します。mix compileを実行してエラーがないことを確認しましょう。(なお,この時点では makeを呼んでいません)

Automakeでライブラリを生成

次にAutomakeの設定をします。

ElixirからSwiftを呼ぶために,ElixirからCで生成したネイティブコードをリンクして呼出すNIFを利用します。NIFで呼出すためには動的ライブラリとして生成しますので,Automakeで動的ライブラリを生成するように設定する必要があります。

Cのソースコードをnative/libnif.cに配置しましょう。次のコマンドを実行します。

swift_elixir_test % mkdir -p native

native/libnif.cを作成します。

native/libnif.c
#include <erl_nif.h>

erl_nif.hというのはNIF APIのヘッダファイルです。

Makefile.amを次のように作成します。

Makefile.am
AUTOMAKE_OPTIONS = subdir-objects
ACLOCAL_AMFLAGS = -I m4

lib_LTLIBRARIES = priv/libnif.la
priv_libnif_la_SOURCES = native/libnif.c

priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS)

priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic

説明は次のとおりです。

  • AUTOMAKE_OPTIONS = subdir-objects でサブディレクトリにソースコード等を配置することを指定します。
  • ACLOCAL_AMFLAGS = -I m4aclocal で設定した値を読み込みます。
  • lib_LTLIBRARIES = priv/libnif.la はビルドしたいライブラリを指定します。拡張子が .la ですが,Automakeでは一律にこのように指定するので,心配しないでください。
  • priv_libnif_la_ というのは priv/libnif.la に対応するオプションであることを示す接頭辞です。
    • priv_libnif_la_SOURCES でソースコードを指定します。ここでは native/libnif.c をコンパイルします。
    • priv_libnif_la_CFLAGS でコンパイルする時の CFLAGS の値を決めます。ここでは,CFLAGSERL_CFLAGS の値を設定します。ERL_CFLAGS は後で configure.acの中で設定しますが,Erlang が提供するヘッダファイルの情報などを定義します。
    • priv_libnif_la_LDFLAGS で同様にリンクする時の LDFLAGS の値を決めます。ここでは,LDFLAGSERL_LDFLAGS の値を設定します。ERL_LDFLAGSは,ERL_CFLAGSと同様です。動的な共有ライブラリを生成するために,-shared -module -export-dynamic を指定します。.so というようにバージョン番号を記載しないようにするために -avoid-version を指定します。

そしてconfigure.acを次のように変更します。

configure.ac
dnl Process this file with autoconf to produce a configure script

AC_INIT([priv/.libs/libnif.so], [1.0])
AC_CONFIG_MACRO_DIRS([m4])
AM_INIT_AUTOMAKE([-Wall -Werror foreign])

AC_ARG_VAR([ELIXIR], [Elixir])
AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR])
AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR])
AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE])
AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS])
AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS])

AC_PROG_CC
AM_PROG_AR

AC_PATH_PROG(ELIXIR, $ELIXIR, elixir)

AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR])
if test "x$ERL_EI_INCLUDE_DIR" = "x"; then
    AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR])

AC_MSG_CHECKING([setting ERL_EI_LIBDIR])
if test "x$ERL_EI_LIBDIR" = "x"; then
    AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_LIBDIR])

AC_MSG_CHECKING([setting ERL_CFLAGS])
if test "x$ERL_CFLAGS" = "x"; then
    AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_CFLAGS])

AC_MSG_CHECKING([setting ERL_LDFLAGS])
if test "x$ERL_LDFLAGS" = "x"; then
    AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_LDFLAGS])

LT_INIT()
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

説明は次のとおりです。

  • AC_INIT に生成するライブラリの情報を与えます。
  • AC_CONFIG_MACRO_DIRS([m4])aclocalで得られた設定を読むようにします。
  • AC_INIT_AUTOMAKE で Automake の使用を宣言します。オプションでエラーや警告を表示するようにしています。
  • AC_ARG_VAR で,configureに与える環境変数を定義します。第1引数に変数名,第2引数にconfigure --helpの時に表示する説明を記載します。本当は第2引数をていねいにドキュメンテーションすべきところですが,手を抜いています。
  • AC_PROG_CCAC_PROG_ARはそれぞれ,CCARで指定されたコンパイラとリンカが存在することを確認します。
  • AC_PATH_PROG(ELIXIR, $ELIXIR, elixir) で環境変数ELIXIRが設定されている場合にはそのパス上のプログラムが,設定されていない時にはelixirが,PATH上に存在するかを確認してその結果を表示します。
  • その後の AC_MSG_CHECKING から AC_MSG_RESULT の一塊は,それぞれErlangに関連する環境変数が設定されているかを確認します。
    • AC_MSG_CHECKING([setting ERL...]) で確認中のメッセージを表示します。
    • if test "x$ERL..." = "x"; then ... fi で環境変数ERL...が設定されているかを確認します。このような書き方は,シェルで移植性の高い記述をするためのAutoconfでは定番の書き方です。
    • AC_SUBSTは第1引数の環境変数に第2引数の値を代入します。
    • ここではelixir --eval ワンライナープログラム とすることで,それぞれ少しずつ異なるElixirのワンライナーのプログラムを実行して設定に必要なパスを取得しています。
    • LC_ALL=en_US.UTF-8 を設定しているのはLinux環境でロケールに関する警告を抑制するためです。
    • AC_MSG_RESULTで設定された結果を表示します。
  • Elixirのワンライナーのプログラムは次のようになっています。
    • :code.root_dir |> to_string()とすることで実行する Erlang の処理系の存在するパスを表示します。この値を仮に$1としましょう。
    • ERL_EI_INCLUDE_DIR: $1/usr/includeを設定します。
    • ERL_EI_LIBDIR: $1/usr/libを設定します。
    • ERL_CFLAGS: ERL_EI_INCLUDE_DIRが設定されているならば -I$ERL_EI_INCLUDE_DIRを,そうでなければ-I$1/usr/includeを設定します。
    • ERL_LDFLAGS: ERL_EI_LIBDIRが設定されているならば -L$ERL_EI_LIBDIRを,そうでなければ-L$1/usr/libを設定します。
  • LT_INIT でLibtoolの初期化をします。
  • AC_CONFIG_FILES([Makefile])Makefileを出力するように設定します。
  • AC_OUTPUTで,以上の結果を出力します。

これらのファイルを記述した後,もしHomebrewを使っているならあらかじめ次のコマンドを実行しておきます。

swift_elixir_test % brew install automake libtool

そして次のコマンドを実行します。

swift_elixir_test % autoreconf -i

.gitignoreに次を追記しましょう。

.gitignore
# For Autoconf
/autom4te.cache/
/Makefile.in
/aclocal.m4
/libtool
/ar-lib
/compile
/install-sh
/ltmain.sh
/m4/
/missing
/depcomp

# For configure
/config.log
/config.status
/config.guess
/config.sub
/configure

# For build files
/native/.deps
/native/.dirstamp
/native/.libs
/native/*.o
/native/*.lo
/priv
Makefile

mix.exsproject情報を次のように変えます。

mix.exs
  def project do
    [
      app: :swift_elixir_test,
      version: "0.1.0",
      elixir: "~> 1.11",
      start_permanent: Mix.env() == :prod,
      deps: deps(),
      compilers: [:elixir_make] ++ Mix.compilers(),
      aliases: [
        compile: [&autoreconf/1, &configure/1, "compile"],
        clean: [&autoreconf/1, &configure/1, "clean"]
      ],
      make_clean: ["clean"]
    ]
  end

また,autoreconfを呼び出すようにmix.exsに次の関数を足します。

mix.exs
  defp autoreconf(_args) do
    System.cmd("autoreconf", ["-i"])
  end

これで mix compile を実行します。エラーなくビルドが終わりましたか? 出来たら次のようにして動的ライブラリが出来上がっていることを確認します。

swift_elixir_test % file priv/.libs/libnif.so 
priv/.libs/libnif.so: Mach-O 64-bit bundle x86_64

やった!

elixir_makeでNIFのビルド

うまくいったので,native/libnif.c を仮実装します。

native/libnif.c
#include <stdlib.h>
#include <erl_nif.h>

static ERL_NIF_TERM test(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[])
{
    ERL_NIF_TERM atom_error = enif_make_atom(env, "error");
    return enif_make_tuple(env, 2, atom_error, enif_make_atom(env, "not_implemented"));
}

static ErlNifFunc nif_funcs[] =
{
    {"test", 0, test}
};

ERL_NIF_INIT(Elixir.SwiftElixirTest, nif_funcs, NULL, NULL, NULL, NULL)

この段階で mix compile してエラーがなくビルドできることを確認します。

説明は次のとおりです。

  • NULLを使うために,#include <stdlib.h>としました。
  • #include <erl_nif.h> はErlang の NIF API を定義しているヘッダファイルをインクルードします。もしここでエラーになるようならば,Automakeの設定のところが間違えていますので,見直してください。
  • test関数の定義はNIF APIに沿っています。第1引数が実行時環境,第2引数と第3引数で可変長の引数を形成しています。ERL_NIF_TERM型はElixir/Erlangの変数のインタフェースです。
  • enif_make_atomでアトムを生成します。第1引数が実行時環境,第2引数がアトムの名前です。
  • enif_make_tupleでタプルを生成します。第1引数が実行時環境,第2引数から可変長の引数を形成していて,第2引数が要素数,第3引数以下が各要素です。
  • 仮に{:error, :not_implemented"}を返しています。
  • nif_funcsで関数を登録します。この場合の意味としてはElixirのtest関数の引数の数(アリティ)が0であるようなtestという名称の関数を定義しています。
  • ERL_NIF_INITでモジュールを登録します。第1引数がモジュール名,第2引数がnif_funcs,第3〜6引数は初期化やリロード時の設定をする関数を登録します。ここでは仮に第3〜6引数にはNULLを登録します。

次にlib/swift_elixir_test.exを変更します。

lib/swift_elixir_test.ex
defmodule SwiftElixirTest do
  require Logger

  @moduledoc """
  Documentation for `SwiftElixirTest`.
  """

  @on_load :load_nif

  def load_nif do
    nif_file = '#{:code.priv_dir(:swift_elixir_test)}/.libs/libnif'

    case :erlang.load_nif(nif_file, 0) do
      :ok -> :ok
      {:error, {:reload, _}} -> :ok
      {:error, reason} -> Logger.warn("Failed to load NIF: #{inspect(reason)}")
    end
  end

  def test(), do: raise("NIF test/0 not implemented")
end

説明は次のとおりです。

  • require Loggerとすることで,デバッグ等のログ出力を行うモジュールを呼び出せるようにします。
  • @on_load :load_nifとすることで,このモジュールを読み込む時,load_nif関数を呼び出します。
  • nif_fileに読み込むNIFライブラリの情報を与えます。
    • nif_file='...'のようにシングルクォーテーションであるのに注意してください。Erlangに直接渡す文字列なので,char listにしてあります。
    • :code_priv_dirは,第1引数で指定したモジュールのprivディレクトリを参照する関数です。
    • SwiftElixirTestモジュールのpriv/.libs/libnif.soを読み込むので,.soを取って:code_priv_dir(:swift_elixir_test)/.libs/libnifとします。
  • :erlang.load_nifはNIFをロードする関数です。
  • :okもしくは{:error, {:reload, ...}}が返ってきた時には正常終了します。
  • それ以外の{:error, ...}が返ってきた時には,...reasonに代入して,Loggerを使って警告表示をします。
  • test関数の定義がNIF関数へのスタブです。NIFを定義する場合の定番で,呼び出した時に例外を発生するようにしています。NIFライブラリが正常に読み込めると上書きされて,NIFを呼び出すようになります。

ここまで出来たら iex -S mix を実行してみましょう。少し待った後に,次のように正常に起動しましたか?

swift_elixir_test % iex -S mix 
Erlang/OTP 23 [erts-11.1.2] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:1] [hipe]

make: Nothing to be done for `all'.
Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

もし実際にビルドしている時には次のように表示されます。

swift_elixir_test % iex -S mix 
Erlang/OTP 23 [erts-11.1.2] [source] [64-bit] [smp:6:6] [ds:6:6:10] [async-threads:1] [hipe]

/bin/sh ./libtool  --tag=CC   --mode=compile gcc -DPACKAGE_NAME=\"priv/.libs/libnif.so\" -DPACKAGE_TARNAME=\"priv--libs-libnif-so\" -DPACKAGE_VERSION=\"1.0\" -DPACKAGE_STRING=\"priv/.libs/libnif.so\ 1.0\" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"priv--libs-libnif-so\" -DVERSION=\"1.0\" -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STRING_H=1 -DHAVE_MEMORY_H=1 -DHAVE_STRINGS_H=1 -DHAVE_INTTYPES_H=1 -DHAVE_STDINT_H=1 -DHAVE_UNISTD_H=1 -DHAVE_DLFCN_H=1 -DLT_OBJDIR=\".libs/\" -I.    -g -O2 -I/Users/zacky/.asdf/installs/erlang/23.1.2/usr/include -g -O2 -MT native/priv_libnif_la-libnif.lo -MD -MP -MF native/.deps/priv_libnif_la-libnif.Tpo -c -o native/priv_libnif_la-libnif.lo `test -f 'native/libnif.c' || echo './'`native/libnif.c
libtool: compile:  gcc -DPACKAGE_NAME=\"priv/.libs/libnif.so\" -DPACKAGE_TARNAME=\"priv--libs-libnif-so\" -DPACKAGE_VERSION=\"1.0\" "-DPACKAGE_STRING=\"priv/.libs/libnif.so 1.0\"" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"priv--libs-libnif-so\" -DVERSION=\"1.0\" -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STRING_H=1 -DHAVE_MEMORY_H=1 -DHAVE_STRINGS_H=1 -DHAVE_INTTYPES_H=1 -DHAVE_STDINT_H=1 -DHAVE_UNISTD_H=1 -DHAVE_DLFCN_H=1 -DLT_OBJDIR=\".libs/\" -I. -g -O2 -I/Users/zacky/.asdf/installs/erlang/23.1.2/usr/include -g -O2 -MT native/priv_libnif_la-libnif.lo -MD -MP -MF native/.deps/priv_libnif_la-libnif.Tpo -c native/libnif.c  -fno-common -DPIC -o native/.libs/priv_libnif_la-libnif.o
libtool: compile:  gcc -DPACKAGE_NAME=\"priv/.libs/libnif.so\" -DPACKAGE_TARNAME=\"priv--libs-libnif-so\" -DPACKAGE_VERSION=\"1.0\" "-DPACKAGE_STRING=\"priv/.libs/libnif.so 1.0\"" -DPACKAGE_BUGREPORT=\"\" -DPACKAGE_URL=\"\" -DPACKAGE=\"priv--libs-libnif-so\" -DVERSION=\"1.0\" -DSTDC_HEADERS=1 -DHAVE_SYS_TYPES_H=1 -DHAVE_SYS_STAT_H=1 -DHAVE_STDLIB_H=1 -DHAVE_STRING_H=1 -DHAVE_MEMORY_H=1 -DHAVE_STRINGS_H=1 -DHAVE_INTTYPES_H=1 -DHAVE_STDINT_H=1 -DHAVE_UNISTD_H=1 -DHAVE_DLFCN_H=1 -DLT_OBJDIR=\".libs/\" -I. -g -O2 -I/Users/zacky/.asdf/installs/erlang/23.1.2/usr/include -g -O2 -MT native/priv_libnif_la-libnif.lo -MD -MP -MF native/.deps/priv_libnif_la-libnif.Tpo -c native/libnif.c -o native/priv_libnif_la-libnif.o >/dev/null 2>&1
mv -f native/.deps/priv_libnif_la-libnif.Tpo native/.deps/priv_libnif_la-libnif.Plo
/bin/sh ./libtool  --tag=CC   --mode=link gcc -g -O2 -I/Users/zacky/.asdf/installs/erlang/23.1.2/usr/include -g -O2  -L/Users/zacky/.asdf/installs/erlang/23.1.2/usr/lib -shared -module -avoid-version -export-dynamic  -o priv/libnif.la -rpath /usr/local/lib native/priv_libnif_la-libnif.lo  
libtool: link: gcc -Wl,-undefined -Wl,dynamic_lookup -o priv/.libs/libnif.so -bundle  native/.libs/priv_libnif_la-libnif.o   -L/Users/zacky/.asdf/installs/erlang/23.1.2/usr/lib  -g -O2 -g -O2  
libtool: link: ( cd "priv/.libs" && rm -f "libnif.la" && ln -s "../libnif.la" "libnif.la" )
Compiling 1 file (.ex)
Generated swift_elixir_test app
Interactive Elixir (1.11.2) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> 

では次のようにSwiftElixirTest.test関数を呼び出して,仮の実装である{:error, :not_implemented}が返ってくることを確かめましょう。

iex(1)> SwiftElixirTest.test
{:error, :not_implemented}
iex(2)> 

Autoconf/AutomakeでMacかどうかを判別するには

まずconfigure.acを次のように書き換えて,OSを認識するようにしましょう。さしあたり,macOSとLinuxで動くようにします。

configure.ac
dnl Process this file with autoconf to produce a configure script

AC_INIT([priv/.libs/libnif.so], [1.0])

AC_CANONICAL_BUILD
AC_CANONICAL_HOST
AC_CANONICAL_TARGET

AC_CONFIG_MACRO_DIRS([m4])
AM_INIT_AUTOMAKE([-Wall -Werror foreign])

AC_ARG_VAR([ELIXIR], [Elixir])
AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR])
AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR])
AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE])
AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS])
AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS])

AC_ARG_VAR([OBJC_FLAGS], [OBJC_FLAGS])

AC_PROG_CC

build_linux=no
build_mac=no
all_mac=no

case "${host_os}" in
    linux*)
        build_linux=yes
        ;;
    cygwin*|mingw*)
        AC_MSG_ERROR([OS $host_os on Windows is not supported])
        ;;
    darwin*)
        case "${build_os}" in
            darwin*)
                case "${target_os}" in
                    darwin*)
                        all_mac=yes
                        AC_PATH_PROG(XCRUN, xcrun)
                        ;;
                    *)
                        ;;
                esac
                ;;
            *)
                ;;
        esac
        build_mac=yes
        ;;
    *)
        AC_MSG_ERROR([OS $host_os is not suppurted])
        ;;
esac

AM_CONDITIONAL([LINUX], [test "x$build_linux" = "xyes"])
AM_CONDITIONAL([OSX], [test "x$build_mac" = "xyes"])
AM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"])

AM_PROG_AR

AC_PATH_PROG(ELIXIR, $ELIXIR, elixir)

AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR])
if test "x$ERL_EI_INCLUDE_DIR" = "x"; then
    AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR])

AC_MSG_CHECKING([setting ERL_EI_LIBDIR])
if test "x$ERL_EI_LIBDIR" = "x"; then
    AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_LIBDIR])

AC_MSG_CHECKING([setting ERL_CFLAGS])
if test "x$ERL_CFLAGS" = "x"; then
    AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_CFLAGS])

AC_MSG_CHECKING([setting ERL_LDFLAGS])
if test "x$ERL_LDFLAGS" = "x"; then
    AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_LDFLAGS])

LT_INIT()
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

追加分を説明します。

  • AC_CANONICAL_BUILD, AC_CANONICAL_HOST, AC_CANONICAL_TARGET を指定することで,それぞれ,ビルド時,ホスト,ターゲットのCPU,ベンダー,OSの情報を得ることが出来るようになります。これらは,AC_INITの直後に置くのが賢明です。
  • OBJC_FLAGSという変数を足しました。
  • build_linux=noからAM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"])までがOSの種類の判別です。さしあたり,私が準備できる検証環境であるLinuxの場合とmacOSの場合にのみビルドができるようにしています。macOSの場合は,さらにビルド時,ホスト,ターゲットがいずれもmacOSの場合でのみ,ALLOSXという条件を成立させるようにしています。
  • また,この条件が成立した時にのみ,Xcodeのコマンドラインツールであるxcrunがパス上に存在するかを確認しています。

次にMakefile.amを次のように変更します。

Makefile.am
AUTOMAKE_OPTIONS = subdir-objects
ACLOCAL_AMFLAGS = -I m4

lib_LTLIBRARIES = priv/libnif.la
priv_libnif_la_SOURCES = native/libnif.c

if ALLOSX
priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS)
else
priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS)
endif

priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic

ALLOSXが成立している場合,すなわちビルド時,ホスト,ターゲットがいずれもmacOSの場合に,マクロALLOSXを定義してnative/libnif.cをコンパイルするようにしています。

これで,native/libnif.c中で #ifdef ALLOSXとすれば,ビルド時,ホスト,ターゲットがいずれもmacOSの場合か,それ以外かを判別してプログラムコードを書き分けることが可能になります。

NIFからObjective-Cのコードを呼び出す

では,ビルド時,ホスト,ターゲットがいずれもmacOSの場合に,次のようなObjective-Cのコードを呼び出してみましょう。

caller.m
#import <Foundation/Foundation.h>
#import "caller.h"

void caller()
{
    NSLog(@"Hello world from Objective-C.");
}
caller.h
#ifndef CALLER_H
#define CALLER_H

void caller();

#endif // CALLER_H

Objective-Cのコードと言いつつ,ほぼCのコードですが,このcaller関数を起点に任意のObjective-Cのコードを呼び出せると思ってください。さしあたり,Foundationに定義されているNSLogを用いて,Hello, worldしたいと思います。

ビルド・リンクするには,Makefile.amを次のようにします。

Makefile.am
AUTOMAKE_OPTIONS = subdir-objects
ACLOCAL_AMFLAGS = -I m4

lib_LTLIBRARIES = priv/libnif.la
priv_libnif_la_SOURCES = native/libnif.c

if ALLOSX
priv_libnif_la_LIBADD = $(LIBOBJS) native/caller.lo
native/caller.lo: native/caller.m native/caller.h
    $(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $<
endif

if ALLOSX
priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS)
else
priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS)
endif

priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic

追加分の説明は次のとおりです。

  • priv_libnif_la_LIBADDとして,priv/.libs/libnif.sonative/caller.loを追加するようにしています。$(LIBOBJS)は,それまでのオブジェクトファイル群を登録している変数です。
  • native/caller.lo: native/caller.m native/caller.hとして依存関係を定義しています。
  • $(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $<とすることで,XcodeのClangを明示的に呼び出してコンパイルし,Libtoolを使って.lo形式に変換しています。XcodeのClangを明示的に呼び出すことで,Frameworkをリンクしてくれますし,Swiftコードをリンクした時にもバージョンの不一致を避けられます。

Foundation と NSLog の動作確認(Objective-C)

せっかくAutotoolsを使っているので,試しにFoundationとNSLogが動作するかをチェックするスクリプトを導入してみましょう。configure.acを次のようにします。

configure.ac
dnl Process this file with autoconf to produce a configure script

AC_INIT([priv/.libs/libnif.so], [1.0])

AC_CANONICAL_BUILD
AC_CANONICAL_HOST
AC_CANONICAL_TARGET

AC_CONFIG_MACRO_DIRS([m4])
AM_INIT_AUTOMAKE([-Wall -Werror foreign])

AC_ARG_VAR([ELIXIR], [Elixir])
AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR])
AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR])
AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE])
AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS])
AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS])

AC_ARG_VAR([OBJC_FLAGS], [OBJC_FLAGS])

AC_PROG_CC

build_linux=no
build_mac=no
all_mac=no

case "${host_os}" in
    linux*)
        build_linux=yes
        ;;
    cygwin*|mingw*)
        AC_MSG_ERROR([OS $host_os on Windows is not supported])
        ;;
    darwin*)
        case "${build_os}" in
            darwin*)
                case "${target_os}" in
                    darwin*)
                        all_mac=yes
                        AC_PATH_PROG(XCRUN, xcrun)
                        ;;
                    *)
                        ;;
                esac
                ;;
            *)
                ;;
        esac
        build_mac=yes
        ;;
    *)
        AC_MSG_ERROR([OS $host_os is not suppurted])
        ;;
esac

AM_CONDITIONAL([LINUX], [test "x$build_linux" = "xyes"])
AM_CONDITIONAL([OSX], [test "x$build_mac" = "xyes"])
AM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"])

AM_PROG_AR

AC_PATH_PROG(ELIXIR, $ELIXIR, elixir)

AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR])
if test "x$ERL_EI_INCLUDE_DIR" = "x"; then
    AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR])

AC_MSG_CHECKING([setting ERL_EI_LIBDIR])
if test "x$ERL_EI_LIBDIR" = "x"; then
    AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_LIBDIR])

AC_MSG_CHECKING([setting ERL_CFLAGS])
if test "x$ERL_CFLAGS" = "x"; then
    AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_CFLAGS])

AC_MSG_CHECKING([setting ERL_LDFLAGS])
if test "x$ERL_LDFLAGS" = "x"; then
    AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_LDFLAGS])

working_foundation=no
working_nslog=no
if test "x$all_mac" = "xyes"; then
    AC_MSG_CHECKING([whether Foundation Framework exists])
    cat>_framework.m<<EOF
#import <Foundation/Foundation.h>
int main() {
    return 0;
}
EOF
    if xcrun clang _framework.m -o _framework > /dev/null 2>&1 && ./_framework > /dev/null 2>&1 ; then
        working_foundation=yes
    fi
    rm -f _framework.m _framework.o _framework
    AC_MSG_RESULT([$working_foundation])

    AC_MSG_CHECKING([whether NSLog works])
    cat>_nslog.m<<EOF
#import <Foundation/Foundation.h>
int main() {
    NSLog(@"hello world");
    return 0;
}
EOF
    if xcrun clang _nslog.m -o _nslog -framework Foundation > /dev/null 2>&1 && ./_nslog  > /dev/null 2>&1 ; then
        working_nslog=yes
    fi
    rm -f _nslog.m _nslog.o _nslog
    AC_MSG_RESULT([$working_nslog])
fi

AM_CONDITIONAL([EXIST_FOUNDATION], [test "x$working_foundation" = "xyes"])
AM_CONDITIONAL([WORK_NSLOG], [test "x$working_nslog" = "xyes"])

LT_INIT()
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

追加分を説明します。

  • working_foundation=noworking_nslog=noでそれぞれのフラグを初期化します。
  • if test "x$all_mac" = "xyes"; thenで,ビルド時,ホスト,ターゲットが全てmacOSのときのみ動作するようにします。
  • AC_MSG_CHECKING([whether Foundation Framework exists])でFoundationが存在するかチェックしますと表示します。
  • 次のcatから2つ目のEOFまでがテストするプログラムコードです。
  • 次のifでこのプログラムコードをコンパイルして実行し,正常終了することを確認します。標準出力とエラー出力をまとめてリダイレクトする点に注意してください。
  • rmで生成したファイルを削除します。
  • AC_MSG_RESULT([$working_foundation])で検証結果を表示します。
  • 同様にNSLogについても動作確認します。
  • AM_CONDITIONALで判定結果をMakefile.amで利用できるようにします。それぞれ,EXIST_FOUNDATIONWORK_NSLOGで真偽値を取り出せるようにしています。

FoundationとNSLogは標準の機能なので,存在をチェックしてコードに反映するのはナンセンスだと思いますが,例としてやってみましょう。

Makefile.am
AUTOMAKE_OPTIONS = subdir-objects
ACLOCAL_AMFLAGS = -I m4

if EXIST_FOUNDATION
OBJC_FLAGS += -DEXIST_FOUNDATION
endif
if WORK_NSLOG
OBJC_FLAGS += -DWORK_NSLOG
endif

lib_LTLIBRARIES = priv/libnif.la
priv_libnif_la_SOURCES = native/libnif.c

if ALLOSX
priv_libnif_la_LIBADD = $(LIBOBJS) native/caller.lo
native/caller.lo: native/caller.m native/caller.h
    $(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $<
endif

if ALLOSX
priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS)
else
priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS)
endif

priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic

追加分の説明です。

  • if EXIST_FOUNDATIONからendifまではEXIST_FOUNDATIONが真の時にOBJC_FLAGS-DEXIST_FOUNDATIONを追加することで,マクロEXIST_FOUNDATIONを定義しています。OBJC_FLAGSの行をインデントしない点に注意してください。インデントすると変数の更新が読まれなくなってしまいます。
  • 同様にif WORK_NSLOGからendifまではWORK_FOUNDAIONが真の時にOBJC_FLAGS-DWORK_NSLOGを追加しています。
caller.m
#ifdef EXIST_FOUNDATION
#import <Foundation/Foundation.h>
#endif

#import "caller.h"

void caller()
{
#ifdef WORK_NSLOG
    NSLog(@"Hello world from Objective-C.");
#endif
}

マクロ EXIST_FOUNDATIONWORK_NSLOGが定義されているかをみて,それぞれ#import <Foundation/Foundation.h>NSLog(...)をスイッチしています。

Objective-CからSwiftのコードを呼び出す

Swift 5.3のコードをObjective-Cから呼び出す方法で既に紹介しましたが,Autoconfに対応させましょう。

次のようなSwiftのコードを呼び出します。このコードの出典はhttps://docs.swift.org/swift-book/LanguageGuide/Methods.html です。

native/ExampleClass.swift
import Foundation

@objc class ExampleClass: NSObject {
    var count = 0
    @objc func increment() {
        count += 1
        NSLog("Hello world from Swift.")
    }
    @objc func increment(by amount: Int) {
        count += amount
    }
    @objc func reset() {
        count = 0
    }
}

Objective-Cから呼び出せるようにするためには,次の2つのことを行います。

  • import Foundationとして,NSObjectから派生するようにクラスを定義する
  • @objcをクラスと,Objective-Cから呼び出したいメソッドに付記する

このようなクラスを足がかりとして,任意のSwiftコードを呼び出せば良いというわけです。

Objective-Cの次のように変更します。

native/caller.m
#import <Foundation/Foundation.h>
#import "ExampleClass-Swift.h"
#import "caller.h"

void caller()
{
    ExampleClass *obj = [[ExampleClass alloc] init];
    [obj increment];
    NSLog(@"Hello world from Objective-C.");
}

ポイントは次のとおりです。

  • クラス名がExampleClassである場合には,import "ExampleClass-Swift.h"とする(クラス名に-Swift.hをつけたヘッダファイルをインポートする)
  • あとはSwiftのコードをObjective-Cに読み替えて呼び出す。

configure.acを次のようにします。

configure.ac
dnl Process this file with autoconf to produce a configure script

AC_INIT([priv/.libs/libnif.so], [1.0])

AC_CANONICAL_BUILD
AC_CANONICAL_HOST
AC_CANONICAL_TARGET

AC_CONFIG_MACRO_DIRS([m4])
AM_INIT_AUTOMAKE([-Wall -Werror foreign])

AC_ARG_VAR([ELIXIR], [Elixir])
AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR])
AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR])
AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE])
AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS])
AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS])

AC_ARG_VAR([OBJC_FLAGS], [OBJC_FLAGS])
AC_ARG_VAR([SWIFT_FLAGS], [SWIFT_FLAGS])

AC_PROG_CC

build_linux=no
build_mac=no
all_mac=no

case "${host_os}" in
    linux*)
        build_linux=yes
        ;;
    cygwin*|mingw*)
        AC_MSG_ERROR([OS $host_os on Windows is not supported])
        ;;
    darwin*)
        case "${build_os}" in
            darwin*)
                case "${target_os}" in
                    darwin*)
                        all_mac=yes
                        AC_PATH_PROG(XCRUN, xcrun)
                        ;;
                    *)
                        ;;
                esac
                ;;
            *)
                ;;
        esac
        build_mac=yes
        ;;
    *)
        AC_MSG_ERROR([OS $host_os is not suppurted])
        ;;
esac

AM_CONDITIONAL([LINUX], [test "x$build_linux" = "xyes"])
AM_CONDITIONAL([OSX], [test "x$build_mac" = "xyes"])
AM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"])

AM_PROG_AR

AC_PATH_PROG(ELIXIR, $ELIXIR, elixir)

AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR])
if test "x$ERL_EI_INCLUDE_DIR" = "x"; then
    AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR])

AC_MSG_CHECKING([setting ERL_EI_LIBDIR])
if test "x$ERL_EI_LIBDIR" = "x"; then
    AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_LIBDIR])

AC_MSG_CHECKING([setting ERL_CFLAGS])
if test "x$ERL_CFLAGS" = "x"; then
    AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_CFLAGS])

AC_MSG_CHECKING([setting ERL_LDFLAGS])
if test "x$ERL_LDFLAGS" = "x"; then
    AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_LDFLAGS])

working_foundation=no
working_nslog=no
if test "x$all_mac" = "xyes"; then
    AC_MSG_CHECKING([whether Foundation Framework exists])
    cat>_framework.m<<EOF
#import <Foundation/Foundation.h>
int main() {
    return 0;
}
EOF
    if xcrun clang _framework.m -o _framework > /dev/null 2>&1 && ./_framework > /dev/null 2>&1 ; then
        working_foundation=yes
    fi
    rm -f _framework.m _framework.o _framework
    AC_MSG_RESULT([$working_foundation])

    AC_MSG_CHECKING([whether NSLog works])
    cat>_nslog.m<<EOF
#import <Foundation/Foundation.h>
int main() {
    NSLog(@"hello world");
    return 0;
}
EOF
    if xcrun clang _nslog.m -o _nslog -framework Foundation > /dev/null 2>&1 && ./_nslog  > /dev/null 2>&1 ; then
        working_nslog=yes
    fi
    rm -f _nslog.m _nslog.o _nslog
    AC_MSG_RESULT([$working_nslog])
fi

AM_CONDITIONAL([EXIST_FOUNDATION], [test "x$working_foundation" = "xyes"])
AM_CONDITIONAL([WORK_NSLOG], [test "x$working_nslog" = "xyes"])

LT_INIT()
AC_CONFIG_FILES([Makefile])
AC_OUTPUT

変更点は次のとおりです。

  • AC_ARG_VAR([SWIFT_FLAGS], [SWIFT_FLAGS])を追加する

本題のMakefile.amを次のようにします。

Makefile.am
AUTOMAKE_OPTIONS = subdir-objects
ACLOCAL_AMFLAGS = -I m4

lib_LTLIBRARIES = priv/libnif.la
priv_libnif_la_SOURCES = native/libnif.c

if ALLOSX
priv_libnif_la_LIBADD = $(LIBOBJS) native/caller.lo native/ExampleClass.lo

native/caller.lo: native/caller.m native/caller.h native/ExampleClass-Swift.h
    $(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $<

native/ExampleClass.lo: native/ExampleClass.swift
    $(LIBTOOL) --mode=compile ./swiftc_wrapper $(SWIFT_FLAGS) -emit-object -parse-as-library $< -o $@ 

native/ExampleClass-Swift.h: native/ExampleClass.swift
    xcrun swiftc $(SWIFT_FLAGS) $< -emit-objc-header -emit-objc-header-path $@
endif

if ALLOSX
priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS)
else
priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS)
endif

if ALLOSX
priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic -L`xcrun --show-sdk-path`/usr/lib/swift -undefined dynamic_lookup
else
priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic
endif

変更点は次のとおりです。

  • priv_libnif_la_LIBADDnative/ExampleClass.loを加える
  • native/caller.loを生成するルールの依存関係に native/ExampleClass-Swift.hを加える
  • native/ExampleClass.lo を生成するルールを加える
    • 依存ファイルはnative/ExampleClass.swift
    • Libtoolでswiftcを呼び出すのですが,そのまま呼び出すとオプションのエラーになるので,ラッパーswiftc_wrapperを介して呼び出す(後述)
    • -emit-objectをつけることでオブジェクトファイルを生成する
    • -parse-as-libraryをつけることでライブラリとしてリンクできるようにする
  • native/ExampleClass-Swift.hを生成するルールを加える
    • xcrun swiftcでSwiftのコンパイラを呼び出す
    • -emit-objc-header でObjective-Cのヘッダファイルを生成させる
    • -emit-objc-header-path で生成先を指定する
    • (残課題) ExampleClass{,.swift{doc,module,sourceinfo}}を生成してしまうのだけど,生成を抑制する方法がわからない
  • ALLOSXのとき,priv_libnif_la_LDFLAGSに以下を加えることで,Swiftのライブラリをリンクする
 -L`xcrun --show-sdk-path`/usr/lib/swift -undefined dynamic_lookup

swiftc_wrapperは次のようなシェルスクリプトです。

#!/bin/sh

echo "xcrun swiftc $@" | sed -e 's/-fno-common//g' | sed -e 's/-DPIC//g' > _swiftc_wrapper
chmod +x _swiftc_wrapper
./_swiftc_wrapper
rm _swiftc_wrapper

やっていることは,Libtoolがコンパイルモードでコンパイラを起動する時につけるオプションである -fno-common-DPICswiftc では認識されないので,外す,ということです。

もうちょっとスマートに書けそうに思います。良い方法があったら教えてください。

以上でmix cleaniex -S mixを実行してコンパイルエラーがないことを確認してください。さらに次のように実行すると,SwiftとObjective-CからそれぞれNSLogでメッセージを表示してくれるはずです。

iex(1)> SwiftElixirTest.test
2020-12-01 06:45:06.883 beam.smp[97578:5144991] Hello world from Swift.
                                                                       2020-12-01 06:45:06.883 beam.smp[97578:5144991] Hello world from Objective-C.
                                                                    :ok
iex(2)> 

Linuxで実行すると次のようになります。

iex(1)> SwiftElixirTest.test
{:error, :not_implemented}
iex(2)> 

Foundation と NSLog の動作確認(Swift)

FoundationとNSLogの動作確認をSwiftでも行いましょう。

configure.ac
dnl Process this file with autoconf to produce a configure script

AC_INIT([priv/.libs/libnif.so], [1.0])

AC_CANONICAL_BUILD
AC_CANONICAL_HOST
AC_CANONICAL_TARGET

AC_CONFIG_MACRO_DIRS([m4])
AM_INIT_AUTOMAKE([-Wall -Werror foreign])

AC_ARG_VAR([ELIXIR], [Elixir])
AC_ARG_VAR([ERL_EI_INCLUDE_DIR], [ERL_EI_INCLUDE_DIR])
AC_ARG_VAR([ERL_EI_LIBDIR], [ERL_EI_LIBDIR])
AC_ARG_VAR([CROSSCOMPILE], [CROSSCOMPILE])
AC_ARG_VAR([ERL_CFLAGS], [ERL_CFLAGS])
AC_ARG_VAR([ERL_LDFLAGS], [ERL_LDFLAGS])

AC_ARG_VAR([OBJC_FLAGS], [OBJC_FLAGS])
AC_ARG_VAR([SWIFT_FLAGS], [SWIFT_FLAGS])

AC_PROG_CC

build_linux=no
build_mac=no
all_mac=no

case "${host_os}" in
    linux*)
        build_linux=yes
        ;;
    cygwin*|mingw*)
        AC_MSG_ERROR([OS $host_os on Windows is not supported])
        ;;
    darwin*)
        case "${build_os}" in
            darwin*)
                case "${target_os}" in
                    darwin*)
                        all_mac=yes
                        AC_PATH_PROG(XCRUN, xcrun)
                        ;;
                    *)
                        ;;
                esac
                ;;
            *)
                ;;
        esac
        build_mac=yes
        ;;
    *)
        AC_MSG_ERROR([OS $host_os is not suppurted])
        ;;
esac

AM_CONDITIONAL([LINUX], [test "x$build_linux" = "xyes"])
AM_CONDITIONAL([OSX], [test "x$build_mac" = "xyes"])
AM_CONDITIONAL([ALLOSX], [test "x$all_mac" = "xyes"])

AM_PROG_AR

AC_PATH_PROG(ELIXIR, $ELIXIR, elixir)

AC_MSG_CHECKING([setting ERL_EI_INCLUDE_DIR])
if test "x$ERL_EI_INCLUDE_DIR" = "x"; then
    AC_SUBST([ERL_EI_INCLUDE_DIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/include") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_INCLUDE_DIR])

AC_MSG_CHECKING([setting ERL_EI_LIBDIR])
if test "x$ERL_EI_LIBDIR" = "x"; then
    AC_SUBST([ERL_EI_LIBDIR], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval ':code.root_dir |> to_string() |> Kernel.<>("/usr/lib") |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_EI_LIBDIR])

AC_MSG_CHECKING([setting ERL_CFLAGS])
if test "x$ERL_CFLAGS" = "x"; then
    AC_SUBST([ERL_CFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-I#{System.get_env("ERL_EI_INCLUDE_DIR", "#{to_string(:code.root_dir)}/usr/include")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_CFLAGS])

AC_MSG_CHECKING([setting ERL_LDFLAGS])
if test "x$ERL_LDFLAGS" = "x"; then
    AC_SUBST([ERL_LDFLAGS], [$(LC_ALL=en_US.UTF-8 $ELIXIR --eval '"-L#{System.get_env("ERL_EI_LIBDIR", "#{to_string(:code.root_dir)}/usr/lib")}" |> IO.puts')])
fi
AC_MSG_RESULT([$ERL_LDFLAGS])

working_foundation=no
working_nslog=no
if test "x$all_mac" = "xyes"; then
    AC_MSG_CHECKING([whether Foundation Framework exists in Objective C])
    cat>_framework.m<<EOF
#import <Foundation/Foundation.h>
int main() {
    return 0;
}
EOF
    if xcrun clang _framework.m -o _framework > /dev/null 2>&1 && ./_framework > /dev/null 2>&1 ; then
        AC_MSG_CHECKING([and Swift])
        cat>_framework.swift<<EOF
import Foundation
import Darwin

exit(0)
EOF
        if xcrun swiftc _framework.swift -o _framework > /dev/null 2>&1 && ./_framework > /dev/null 2>&1 ; then
            working_foundation=yes
        fi
    fi
    rm -f _framework.swift _framework.m _framework.o _framework
    AC_MSG_RESULT([$working_foundation])

    AC_MSG_CHECKING([whether NSLog works in Objective-C])
    cat>_nslog.m<<EOF
#import <Foundation/Foundation.h>
int main() {
    NSLog(@"hello world");
    return 0;
}
EOF
    if xcrun clang _nslog.m -o _nslog -framework Foundation > /dev/null 2>&1 && ./_nslog  > /dev/null 2>&1 ; then
        AC_MSG_CHECKING([and Swift])
        cat>_nslog.swift<<EOF
import Foundation
import Darwin

NSLog("hello world")
exit(0)
EOF
        if xcrun swiftc _nslog.swift -o _nslog > /dev/null 2>&1 && ./_nslog  > /dev/null 2>&1 ; then
            working_nslog=yes
        fi
    fi
    rm -f _nslog.swift _nslog.m _nslog.o _nslog
    AC_MSG_RESULT([$working_nslog])
fi

AM_CONDITIONAL([EXIST_FOUNDATION], [test "x$working_foundation" = "xyes"])
AM_CONDITIONAL([WORK_NSLOG], [test "x$working_nslog" = "xyes"])

LT_INIT()
AC_CONFIG_FILES([Makefile])
AC_OUTPUT
Makefile.am
AUTOMAKE_OPTIONS = subdir-objects
ACLOCAL_AMFLAGS = -I m4

if EXIST_FOUNDATION
OBJC_FLAGS += -DEXIST_FOUNDATION
SWIFT_FLAGS += -DEXIST_FOUNDATION
endif
if WORK_NSLOG
OBJC_FLAGS += -DWORK_NSLOG
SWIFT_FLAGS += -DWORK_NSLOG
endif

lib_LTLIBRARIES = priv/libnif.la
priv_libnif_la_SOURCES = native/libnif.c

if ALLOSX
priv_libnif_la_LIBADD = $(LIBOBJS) native/caller.lo native/ExampleClass.lo

native/caller.lo: native/caller.m native/caller.h native/ExampleClass-Swift.h
    $(LIBTOOL) --mode=compile xcrun clang -c $(OBJC_FLAGS) $(CFLAGS) -o $@ $<

native/ExampleClass.lo: native/ExampleClass.swift
    $(LIBTOOL) --mode=compile ./swiftc_wrapper $(SWIFT_FLAGS) -emit-object -parse-as-library $< -o $@ 

native/ExampleClass-Swift.h: native/ExampleClass.swift
    xcrun swiftc $(SWIFT_FLAGS) $< -emit-objc-header -emit-objc-header-path $@
endif

if ALLOSX
priv_libnif_la_CFLAGS = -DALLOSX $(CFLAGS) $(ERL_CFLAGS)
else
priv_libnif_la_CFLAGS = $(CFLAGS) $(ERL_CFLAGS)
endif

if ALLOSX
priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic -L`xcrun --show-sdk-path`/usr/lib/swift -undefined dynamic_lookup
else
priv_libnif_la_LDFLAGS = $(LDFLAGS) $(ERL_LDFLAGS) -shared -module -avoid-version -export-dynamic
endif
native/ExampleClass.swift
#if EXIST_FOUNDATION
import Foundation
#endif

@objc class ExampleClass: NSObject {
    var count = 0
    @objc func increment() {
        count += 1
#if WORK_NSLOG
        NSLog("Hello world from Swift.")
#endif
    }
    @objc func increment(by amount: Int) {
        count += amount
    }
    @objc func reset() {
        count = 0
    }
}

だいたいObjective-Cの場合と同様ですが,いくつか違いがあります。

  • swiftcでは-framework Foundationは不要です。
  • Cでの#ifdefはSwiftでは#ifです。

おわりに

この記事ではElixirからObjective-CやSwift 5.3のコードを呼び出す方法について詳説しました。またAutotoolsを使ってObjective-CやSwiftのヘッダファイルや関数が存在するかどうかを確認する方法も示しています。

将来課題としては,Autotoolsを使うとビルドに時間がかかるようになってくるので,make -jによる並列コンパイルをしたり,必要な時だけautoreconf./configureをするように改めたいのと,もっと長期的にはAutotoolsの代わりをするElixirベースのビルドツールを構築したいなと思っています。

あと,M1 Macだったときにはユニバーサルバイナリを生成するようにしてみたいですね。

  • ユニバーサルバイナリの生成方法はBuilding a Universal macOS Binaryに書かれています。
  • また,system_profiler SPSoftwareDataTypeとするとSystem Versionの項目にmacOSのバージョンが出ます。

以上を利用して,Big Surは macOS 11.0なので,それ以降だったらx86_64とarm64のユニバーサルバイナリを生成するというロジックでも良いかと思います。この方法はまた後日試してみたいと思います。

明日のElixir Advent Calendar 2020 11日目の記事は @pojiro さんの「Elixirで並行コマンド実行サーバーを作ったら感動した話」です。よろしくお願いします。

本研究成果は、科学技術振興機構研究成果展開事業研究成果最適展開支援プログラム A-STEP トライアウト JPMJTM20H1 の支援を受けた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Apple Silicon M1チップ搭載Mac (Big Sur) に Elixir / Erlang をクリーンインストールする〜Elixir/Pelemayマイクロベンチマーク結果もあるよ!(2020.12.4現在版)

この記事はElixir Advent Calendar 2020の6日目です。

昨日は @ShozF さんの「Elixir練習帳: .npyファイルの中を覗く」でした。

さて,今日はおまちかね,Apple Silicon M1チップ搭載のMacにElixirとErlangをクリーンインストールしてみたという記事です。Elixir/Pelemayのマイクロベンチマークも走らせてみました。そのパフォーマンスの高さに驚いてくださいませ。

インストール状況について,結論から言うと

インストール方法 Rosetta 2 ARM ネイティブ
Homebrew
asdf △?
kerl ○? ×

?がついているところは2020年12月4日現在で完全には確認できていないところです。そのうち追って確認します。

追記(2020/12/7): ARMネイティブの時のkerlでのインストールがうまく動かないことがわかりました。引き続き検証します。

追記(2020/12/7): ARMネイティブでasdfでインストールした場合,Rosetta 2モードでターミナルを起動しても,Elixirを実行できることがわかりました。

追記(2020/12/8): しかし↑この場合(ARMネイティブでasdfをインストールしてRosetta2モードでターミナルを起動した場合),今まで知られている方法でNIFをコンパイルして実行しようとすると,x86_64のバイナリでコンパイル・リンクしようとするため,不一致のためNIFをロード・実行できないという問題があることがわかりました。

HomebrewでのElixirインストール手順

  1. Xcodeをインストールする
  2. Xcodeを起動してコンポーネントをインストールする
  3. ターミナルで xcode-select --install を実行する
  4. Homebrew をインストールする (Rosetta2の場合とArmネイティブの場合)
  5. brew install elixir

asdfでのElixirインストール手順

下記の記事通りです。

https://qiita.com/zacky1972/items/ad2545fe8414bbd177a0

注意点としては,原理上,Rosetta 2 モードと ARM ネイティブモードを両立できないという点です。asdfの抜本的な改造が必要でしょうね。というわけで,issue 書きました。 https://github.com/asdf-vm/asdf/issues/834

追記(2020/12/8): ↑issueに否定的なコメントがつきましたね。理解はできます。

kerlでのElixirインストール手順

下記の記事通りです。

https://qiita.com/zacky1972/items/ad2545fe8414bbd177a0

ARMネイティブモードの場合,export KERL_CONFIGURE_OPTIONS="--with-ssl=/opt/homebrew/opt/openssl@1.1 --enable-darwin-64bit"とします。

kerl build 23.1.4 23.1.4_Rosetta2, kerl build 23.1.4 23.1.4_ARM みたいにすると,Rosetta 2 モードと ARM ネイティブモードを共存できます。

Xcodeを起動するときの注意点

https://zenn.dev/paraches/articles/m1-xcode-rosetta2 を参照のこと

要はリターンキーを押せば先に進みます。

Rosetta2 の Homebrew のインストール方法

  1. アプリケーション/ユーティリティ フォルダを開いてターミナルの「情報を見る」を開きます。
  2. 「Rosettaを使用して開く」にチェックを入れます。
  3. ターミナルを起動します。
  4. あとは通常のHomebrewのインストール手順です。

通常と同じく/usr/local配下にインストールされます。

ちなみに,「Rosettaを使用して開く」にチェックを入れていても,sshで外部からログインしたときには,そのシェルではRosettaは有効にならず,ARMネイティブモードになります。

ARMネイティブのHomebrewのインストール方法

https://github.com/mikelxc/Workarounds-for-ARM-mac

  1. アプリケーション/ユーティリティ フォルダを開いてターミナルの「情報を見る」を開きます。
  2. 「Rosettaを使用して開く」のチェックを外します。
  3. ターミナルを起動します。
  4. cd ~
  5. mkdir homebrew && curl -L https://github.com/Homebrew/brew/tarball/master | tar xz --strip 1 -C homebrew
  6. sudo mv homebrew /opt/homebrew
  7. cd /opt/homebrew/bin
  8. ./brew update
  9. .zshrc に export PATH="/opt/homebrew/bin:$PATH"を記述する
  10. source .zshrc

通常と異なり,/opt/homebrew配下にインストールされます。

おまけ: ベンチマーク結果

次のような自作のベンチマークプログラムを走らせてみました。

https://github.com/zeam-vm/pelemay_sample

Pelemay 0.0.14になってARMネイティブモードのM1 Macで動くようになりました。

参考: iMac Pro 2017 (CPU/GPU全部盛り) での実行結果

## FloatMultBench
benchmark name          iterations   average time 
Pelemay                      50000   38.77 µs/op
Enum                         10000   233.19 µs/op
Flow                           500   3368.38 µs/op
## LogisticMapBench
benchmark name          iterations   average time 
Pelemay                       5000   624.14 µs/op
Enum                          1000   2386.87 µs/op
Flow                           500   5600.73 µs/op
## StringReplaceBench
benchmark name          iterations   average time 
Pelemay String.replace    10000000   0.80 µs/op
Enum String.replace        1000000   2.61 µs/op
Flow String.replace           1000   1999.50 µs/op

Mac mini (M1, 2020) ARM モードでの実行結果

benchmark name          iterations   average time 
Pelemay                     100000   28.60 µs/op
Enum                         10000   119.89 µs/op
Flow                          1000   1021.03 µs/op
## LogisticMapBench
benchmark name          iterations   average time 
Pelemay                       5000   377.24 µs/op
Enum                          2000   897.06 µs/op
Flow                          1000   2265.86 µs/op
## StringReplaceBench
benchmark name          iterations   average time 
Pelemay String.replace    10000000   0.66 µs/op
Enum String.replace        1000000   1.45 µs/op
Flow String.replace          10000   252.52 µs/op

Mac mini (M1, 2020) Rosetta 2 モードでの実行結果

Pelemay が,たとえバージョン0.0.14であっても,バグで動かなかったので,削除してあります。issueを立ててありますので,詳細をご覧になりたい方はどうぞ。
https://github.com/zeam-vm/pelemay/issues/154

## FloatMultBench
benchmark name       iterations   average time 
Enum                      10000   278.17 µs/op
Flow                       1000   1725.31 µs/op
## LogisticMapBench
benchmark name       iterations   average time 
Enum                       1000   2272.93 µs/op
Flow                        500   4115.19 µs/op
## StringReplaceBench
benchmark name       iterations   average time 
Enum String.replace     1000000   2.39 µs/op
Flow String.replace        5000   388.05 µs/op

結果

いずれも ARMモードの Mac mini (M1, 2020) の方が iMac Pro 2017 よりかなり高速でした。Pelemayの速度向上は大体C言語によるネイティブのコードの速度向上比と同程度と考えられます。一方,Enumの速度向上はシングルコアで動作する標準的なインタプリタの場合の速度向上比と考えられます。Flowの速度向上は,マルチコアで動作する標準的なインタプリタの場合の速度向上比と考えられます。シングルコア性能も高いですが,とくにマルチコア性能は高いですね!

Pelemay iMac Pro 2017 Mac mini (M1, 2020) ARM 倍率
整数演算 624.14 µs/op 377.24 µs/op 1.65倍
浮動小数点数演算 38.77 µs/op 28.60 µs/op 1.36倍
文字列置換 0.80 µs/op 0.66 µs/op 1.21倍
Enum iMac Pro 2017 Mac mini (M1, 2020) ARM 倍率
整数演算 2386.87 µs/op 897.06 µs/op 2.66倍
浮動小数点数演算 233.19 µs/op 119.89 µs/op 1.95倍
文字列置換 2.61 µs/op 1.45 µs/op 1.80倍
Flow iMac Pro 2017 Mac mini (M1, 2020) ARM 倍率
整数演算 5600.73 µs/op 2265.86 µs/op 2.47倍
浮動小数点数演算 3368.38 µs/op 1021.03 µs/op 3.30倍
文字列置換 1999.50 µs/op 252.52 µs/op 7.92倍

Rosetta 2 モードの Mac mini (M1, 2020) と iMac Pro 2017 はEnumでほぼ互角,FlowではMac miniの方が速いです。この場合も,マルチコアであるFlowの場合の方が速度向上比が高かったです。

Enum iMac Pro 2017 Mac mini (M1, 2020) Rosetta 2 倍率
整数演算 2386.87 µs/op 2272.93 µs/op 1.05倍
浮動小数点数演算 233.19 µs/op 278.17 µs/op 0.838倍
文字列置換 2.61 µs/op 2.39 µs/op 1.09倍
Flow iMac Pro 2017 Mac mini (M1, 2020) Rosetta 2 倍率
整数演算 5600.73 µs/op 4115.19 µs/op 1.36倍
浮動小数点数演算 3368.38 µs/op 1725.31 µs/op 1.95倍
文字列置換 1999.50 µs/op 388.05 µs/op 5.15倍

Rosetta 2 より ARM ネイティブは1.6〜2.5倍程度高速であると言えそうです。

Mac mini (M1, 2020) Enum Rosetta 2 ARM 倍率
整数演算 2272.93 µs/op 897.06 µs/op 2.53倍
浮動小数点数演算 278.17 µs/op 119.89 µs/op 2.32倍
文字列置換 2.39 µs/op 1.45 µs/op 1.65倍
Mac mini (M1, 2020) Flow Rosetta 2 ARM 倍率
整数演算 4115.19 µs/op 2265.86 µs/op 1.95倍
浮動小数点数演算 1725.31 µs/op 1021.03 µs/op 1.69倍
文字列置換 388.05 µs/op 252.52 µs/op 1.54倍

おわりに

明日のElixir Advent Calendar 2020 7日目の記事は @koyo-miyamura さんです。よろしくお願いします。

本研究成果は、科学技術振興機構研究成果展開事業研究成果最適展開支援プログラム A-STEP トライアウト JPMJTM20H1 の支援を受けた。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

デザイナーがM1のMacBookAirで満足できるか

201203_02.png
土曜日の昼下がり、玄関先でごそっと物音をたてて、金属のラックが少し揺れた。
置き配用の金属ラックの3段目、飾り気のない段ボールをひとつ。その中には見覚えがあるソリッドの立った白い箱があり、それを右隅からビニールを心地剥がすと前回の記憶が蘇る。

──正直そこまで期待していなかった
世間的には「奇跡的だ」「魔法の絨毯だ」とブログ、YouTube界隈で過剰に騒ぎ立てて、疑心暗鬼になっていたが、旧MacBookから移行アシスタントが終了し、10分後にはその考えが覆った。

──感想は「テスラ・モーターズ」
起動速度、静音性、バッテリー性能、発熱性、どれをとっても新しく感じ、触った感想は「テスラ」(乗ったことないけど)。昨日まで使用していたMacはリッター15kmのワゴンだと感じた。もちろん、Rosetta2を還してのアプリケーションも多く、ユニバーサルバージョンにまだ移行していないアプリでこの速度だと半年後がさらに楽しみになってくる。

──Apple M1チップ
このプロダクトはAppleが独自で開発した「M1チップ」に集約されていると言っても過言ではない。(というか、他はほとんど変わっていない)プロセッサ、I/O、セキュリティ、メモリなどを1枚のシステムオンチップ(SoC)に集めたのがM1チップで効率が上がり、驚異的な性能を発揮している。またiPhoneやiPadに搭載されているプロセッサーが同じということもあり、iPhoneやiPadのアプリケーションも動かせるのも大きい。
 2020-12-04 9.33.51.png

──飛ぶように早い
驚くのが2019年に販売された、ほとんどのMacのよりも早いということ。まず初速度が違う。これはユニファイドメモリアーキテクチャ(UMA)から来たもので、専用パッケージの中にある1つのプールに入れて、SoCにあるデータにアクセスでき、複数のメモリプールの間でデータをコピーする必要がなくなったためだと思われる。
 2020-12-04 10.03.02.png

──静寂そのものの
筆者がこのMacBookAirに「テスラ」のメタファを用いるのはファンそのものがない静寂性を上げている。どれだけ負荷をかけた作業をしていても一切の音がしないのだ。想像して欲しい。今持っているiphoneにyoutubeなどで負荷をかけると高周波でファンのが鳴り出したら気が狂う。そう考えるとファンがある元のMacには戻れない。
 2020-12-04 9.31.17.png

──デザイナーはメイン機として使用できるか
正直、世の中の90%ぐらいのデザイナーは一番安いモデル(12月3日時点で104,800円(税別))で満足できると思う。SSDの容量はクラウドで補完すれば良い。メモリは8GBと心許ない感じがするが、SoCにメモリが統合された「ユニファイドメモリ」が効率的に作用する。たとえスワップしたとしても、SSDの性能が向上でカバーできている。

──購入すべきデザイナー(2つ以上該当する場合)
 ・所有Macのマルチコアベンチマークが3,500以下(https://browser.geekbench.com/mac-benchmarks
 ・Macのメモリが8GB以下
 ・macOS Big Surにアップデートをしている(予定がある)
 ・20インチ以上のディスプレイを所有している(予定がある)
 ・すごくお金がある人

──プロとアマチュアの境界が消えていく
今まで、Macで「Pro」を所有するというのは、デザイナーの自己顕示欲のようなものだったのは間違いない。30年ほど前、MacintoshはDTPの道を切り開き、デザイナーの道具の境界線を消してきた。それ以前は1mmの線の上に10本の線を烏口で引くことや、写植、活版など知識、技術がなければ、印刷物などに辿り着かなかったから。しかし、今現在では20万円ほどでプロと並ぶほどの道具は揃えられるし、ある程度PCを触れる主婦なら、3日ほどでラクスルへ入稿できる。そういった意味で確実にハードルが下がってきている。そういった中で、デザイナーはこれからどのようなバリューを提供していかなければいけないか、考え直す必要があると思う。
 2020-12-04 10.26.29.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む