この記事は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 %
ここで自動生成されたコードを修正しておきます。
defmodule SwiftElixirTest do
@moduledoc """
Documentation for `SwiftElixirTest`.
"""
@doc """
Hello world.
## Examples
iex> SwiftElixirTest.hello()
:world
"""
def hello do
:world
end
end
これを次のようにします。(hello
関数を削除)
defmodule SwiftElixirTest do
@moduledoc """
Documentation for `SwiftElixirTest`.
"""
end
そして自動生成されたテストコードも修正します。
defmodule SwiftElixirTestTest do
use ExUnit.Case
doctest SwiftElixirTest
test "greets the world" do
assert SwiftElixirTest.hello() == :world
end
end
これを次のようにします。
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
を作成します。
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
が追加ファイルを無視するようにしましょう。
# For Autoconf
/autom4te.cache/
# For configure
/configure
configure
を実行してみます。
swift_elixir_test % ./configure
するとconfig.log
が生成されるので,これも.gitignore
に下記を追記して無視するように設定します。
# For configure
/config.log
/configure
elixir_make
でconfigure
を呼ぶ
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
の下記の部分がインストールするライブラリを指定する部分です。
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
これを次のように書き換えます。
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` を実行することになります。
defp configure(_args) do
System.cmd("#{File.cwd!()}/configure", [])
end
そしてmix.exs
の下記の部分がプロジェクト情報なのですが,これを書き換えます。
def project do
[
app: :swift_elixir_test,
version: "0.1.0",
elixir: "~> 1.11",
start_permanent: Mix.env() == :prod,
deps: deps()
]
end
次のようにします。
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
を作成します。
#include <erl_nif.h>
erl_nif.h
というのはNIF APIのヘッダファイルです。
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 m4
はaclocal
で設定した値を読み込みます。 -
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
の値を決めます。ここでは,CFLAGS
とERL_CFLAGS
の値を設定します。ERL_CFLAGS
は後でconfigure.ac
の中で設定しますが,Erlang が提供するヘッダファイルの情報などを定義します。 -
priv_libnif_la_LDFLAGS
で同様にリンクする時のLDFLAGS
の値を決めます。ここでは,LDFLAGS
とERL_LDFLAGS
の値を設定します。ERL_LDFLAGS
は,ERL_CFLAGS
と同様です。動的な共有ライブラリを生成するために,-shared
-module
-export-dynamic
を指定します。.so
というようにバージョン番号を記載しないようにするために-avoid-version
を指定します。
-
そして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_CC
とAC_PROG_AR
はそれぞれ,CC
とAR
で指定されたコンパイラとリンカが存在することを確認します。 -
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
に次を追記しましょう。
# 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.exs
のproject
情報を次のように変えます。
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
に次の関数を足します。
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
を仮実装します。
#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
を変更します。
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で動くようにします。
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
を次のように変更します。
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のコードを呼び出してみましょう。
#import <Foundation/Foundation.h>
#import "caller.h"
void caller()
{
NSLog(@"Hello world from Objective-C.");
}
#ifndef CALLER_H
#define CALLER_H
void caller();
#endif // CALLER_H
Objective-Cのコードと言いつつ,ほぼCのコードですが,このcaller関数を起点に任意のObjective-Cのコードを呼び出せると思ってください。さしあたり,Foundation
に定義されているNSLog
を用いて,Hello, worldしたいと思います。
ビルド・リンクするには,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.so
にnative/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
を次のようにします。
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=no
とworking_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_FOUNDATION
とWORK_NSLOG
で真偽値を取り出せるようにしています。
FoundationとNSLog
は標準の機能なので,存在をチェックしてコードに反映するのはナンセンスだと思いますが,例としてやってみましょう。
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
を追加しています。
#ifdef EXIST_FOUNDATION
#import <Foundation/Foundation.h>
#endif
#import "caller.h"
void caller()
{
#ifdef WORK_NSLOG
NSLog(@"Hello world from Objective-C.");
#endif
}
マクロ EXIST_FOUNDATION
とWORK_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 です。
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の次のように変更します。
#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
を次のようにします。
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
を次のようにします。
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_LIBADD
にnative/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
と -DPIC
は swiftc
では認識されないので,外す,ということです。
もうちょっとスマートに書けそうに思います。良い方法があったら教えてください。
以上でmix clean
と iex -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でも行いましょう。
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
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
#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 の支援を受けた。