LoginSignup
21
2

More than 3 years have passed since last update.

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

Last updated at Posted at 2020-12-09

この記事は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 の支援を受けた。

21
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
2