Common Lisp implementation の大多数には、C ABI を使う library の function を呼び出せる何らかの foreign function interface があります。しかし逆方向、つまり CL library を他の言語から C ABI 経由で呼び出せる library として compile することは、あまり一般的ではないかもしれません。
LispWorks や Allegro CL のような commercial implementation は通常この機能を提供しており、document もよく整備されています 1。
この章では、SBCL-Librarian という project を説明します。これは、優れた open-source かつ free-to-use な implementation である SBCL (Steel Bank Common Lisp) を使い、C (C FFI を持つもの全般) と Python から呼び出せる library を作るための、方針を持った方法です。
SBCL-Librarian は callback を support しているため、すばらしい machine learning や statistical library を使う Python code など、どんな code とでも Lisp library を統合できます。
SBCL-Librarian の動作は、C source file、C header、Python module を生成するというものです。
C source file はまず dynamic library に compile されます。この library は、提供される header file を使って、任意の C project から、または C library の loading を support する言語の任意の project から load できます。
生成された Python module は、compile 済み library を Python process に load します。つまり、Lisp library を Python code から使う前に C library を compile しておく必要があります。この事実には、主に 2 つの結果があります。
- 一方で、Lisp library はすべて効率的な native code になります。これはすばらしいことです。Python interpreter はかなり遅いことがあり、特に machine learning や statistics の library の多くは native code に compile されています。Common Lisp でも同じ効率を達成できます。
- 他方で、library は Python と通信するために C interface だけを使えます。C の primitive data type、structure、function、pointer (function への pointer を含む) です。C の基本知識が必要です。
Environment の準備
Shared Library Support 付きで SBCL を build する
SBCL の binary distribution は通常、shared library として build された SBCL を含んでいません。しかしこれは SBCL-Librarian に必要です。
SBCL git repository から download するか、Roswell を使って ros install sbcl-source command を実行して取得できます。
SBCL は compilation process を bootstrap するために、動作する Common Lisp system も必要とします。簡単な trick は、Roswell から binary installation を download し、それを PATH variable に追加することです。
SBCL は zstd library に依存します。Linux-based system では、library と header file の両方を package manager から取得できます。通常は libzstd-dev という名前です。Windows では、推奨される方法は
MSYS2 を使うことです。MSYS2 には Roswell、zstd、その header が含まれます。
source のある directory へ移動し、次を実行します。
# Bash
# (assuming the version of your SBCL installed via Roswell is 2.4.1)
export PATH=~/.roswell/impls/x86-64/linux/sbcl-bin/2.4.1/bin/:$PATH
./make-config.sh --fancy
./make.sh --fancy
./make-shared-library.sh --fancy
shared library は Windows や Mac でも .so extension を持つことに注意してください。ただし、問題なく動くようです。MSYS2 で Roswell を使う場合、MSYS2 home directory ではなく Windows home directory を使うことがあります。これらは異なる path です。したがって、Roswell への path は ~/.roswell/ ではなく $USERPROFILE/.roswell (または /C/Users/<username>/.roswell) かもしれません。
SBCL-Librarian を download して setup する
SBCL-Librarian repository を clone します。
git clone https://github.com/quil-lang/sbcl-librarian.git
Lisp からの Hello World
SBCL-Librarian にはいくつかの documentation といくつかの example が付属していますが、基本 tutorial のようなものは実際にはありません。この章では、2 つの数を足す基本的な function を作り、それを Python から呼び出します。
便利のため、environment variable をいくつか設定しましょう。
# Directory with SBCL sources
export SBCL_SRC=~/.roswell/src/sbcl-2.4.1
# Directory with this project, don't forget the double slash at the end
# or it might not work
export CL_SOURCE_REGISTRY="~/prg/sbcl-librarian//"
より新しい Linux-based system では、通常、現在の directory は library の検索対象になりません。Python が library を検索する path も、通常は current working directory に設定されていません。便利のため、次のように設定します。
export LD_LIBRARY_PATH=.:
export PATH=.:$PATH
これで、次の内容を持つ helloworld.lisp file を作成できます。
(require '#:asdf)
(asdf:load-system :sbcl-librarian)
(defpackage libhelloworld
(:use :cl :sbcl-librarian))
(in-package libhelloworld)
;; will be called from Python
(defun hello-world (a b)
(+ a b))
;; error enum to be used in C/Python code for error handling
(define-enum-type error-type "err_t"
("ERR_SUCCESS" 0)
("ERR_FAIL" 1))
;; mapping Common Lisp conditions to C enums
;; in this simple example, all conditions are mapped to number 1
;; which is "ERR_FAIL" in `error-type` enum
(define-error-map error-map error-type 0
((t (lambda (condition)
(declare (ignore condition))
(return-from error-map 1)))))
;; structure of the generated C source file
(define-api libhelloworld-api (:error-map error-map ; error enum
:function-prefix "helloworld_") ; prefix for all function names (C doesn't have namespaces)
(:literal "/* types */") ; just a comment (whatever is there will be printed as-is)
(:type error-type) ; outputs the error enum
(:literal "/* functions */")
(:function ; function declaration - name, return type, argument types
(hello-world :int ((a :int) (b :int)))))
;; definition of the whole library - what is there
(define-aggregate-library libhelloworld (:function-linkage "LIBHELLOWORLD_API")
sbcl-librarian:handles sbcl-librarian:environment libhelloworld-api)
;; builds the bindings
(build-bindings libhelloworld ".")
(build-python-bindings libhelloworld ".")
;; outputs the Lisp core
(build-core-and-die libhelloworld "." :compression t)
macro define-enum-type は、Common Lisp function が signal する condition と、wrapping C function の return type との mapping を作ります。Common Lisp から condition が signal されると、define-error-map の中で数値、つまり C function return value に変換されます。enumeration type は C enum を追加するため、次の代わりに:
if (1 == cl_function()) {
次のように書けます。
if (ERR_FAIL == cl_function()) {
こちらのほうが読みやすいです。
define-api は、作成される library code の structure を概説し、error map、type、function、およびそれらの順序を指定します (この場合、:literal は comment に使われます)。
define-aggregate-library は、library 全体を定義し、何をどの順序で含めるかを指定します。
次の command で file を compile できます。
$SBCL_SRC/run-sbcl.sh --script "helloworld.lisp"
cc -shared -fpic -o libhelloworld.so libhelloworld.c -L$SBCL_SRC/src/runtime -lsbcl
cp $SBCL_SRC/src/runtime/libsbcl.so .
Python console を起動し、helloworld module が正常に作成されたことを確認できます。
import helloworld
dir(helloworld)
print された dictionary に function helloworld_hello_world が存在するはずです。
この function は、function の return value が error code になるという C standard に従います
(0 は success、その他の数値は error-map definition に従う err_t class で定義されるべきです)。
function の最後の parameter が、その return value です。この場合これは integer への pointer なので、
ctypes library を使って integer を作成し、result value への pointer とともに helloworld_hello_world を呼び出す必要があります。
次の program は 11 を print するはずです。
import helloworld
import ctypes
rv = ctypes.c_int(0)
helloworld.helloworld_hello_world(5, 6, ctypes.pointer(rv))
print(rv.value)
system によって、よく起きる問題が 2 つあります。
1 つ目は Python からのややわかりにくい error です。
>>> import helloworld
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: dynamic module does not define module export function (PyInit_helloworld)
これは、Python が helloworld.py ではなく helloworld.so を Python module として開こうとしていることを意味します。helloworld.so は natively-compiled Python module ではなく普通の dynamic library なので、これは動きません。
cp ./helloworld.py ./py_helloworld.py
そして Python では import py_helloworld します。
次の exception が raise される場合:
Traceback (most recent call last):
...
raise Exception('Unable to locate libhelloworld') from e
Exception: Unable to locate libhelloworld
まず、required dependency、ここでは libsbcl と libzstd が、output directory に copy されているか、operating system が library を load する path にあるかを確認してください。それでも動かない場合は、あなたの particular system で Python が library を locate する mechanism の問題かもしれません。
helloworld.py (または先ほどの提案どおり rename した場合は py_helloworld.py) を開き、次の行を
libpath = Path(find_library('libcallback')).resolve()
あなたの operating system 用の path に変更します。例:
libpath = Path('./libhelloworld.so').resolve()
より複雑な例: Callback Example
SBCL-Librarian には複数の example が含まれており、そのうちの 1 つは Python code への単純な callback です。この example には Makefile が付属し、asdf を使って適切に定義された system もあります。
ASDF system definition
libcallback.asd の system definition は、SBCL-Librarian への dependency を宣言します。
(asdf:defsystem #:libcallback
:defsystem-depends-on (#:sbcl-librarian)
:depends-on (#:sbcl-librarian)
ASDF system は SBCL-Librarian source をどこで見つけるかを知る必要があります。これを指定する方法の 1 つは、上で見たように、その directory を含むよう CL_SOURCE_REGISTRY environment variable を設定することです。あるいは、ASDF が見つけられる location (~/common-lisp/, ~/quicklisp/local-projects/) に project を clone します。
Bindings.lisp
bindings.lisp には、C binding を生成するための重要な要素が含まれます。
(defun call-callback (callback outbuffer)
(sb-alien:with-alien ((str sb-alien:c-string "I guess "))
(sb-alien:alien-funcall callback str outbuffer)))
この function は example の要です。Python code から invoke され、Python method (callback parameter) を call back します。SBCL-Librarian は C library と、それを wrap する Python module の両方を生成するため、この function は C からも Python からも呼び出せます。この example は Python に焦点を当てます。
SBCL-Librarian は、C function と連携するための SBCL package である sb-alien を利用します。with-alien は、その scope 内で有効で、終了後に自動的に破棄される resource (ここでは type c-string の str) を作成し、memory leak を防ぎます。alien-funcall は C function を呼び出すために使われ、この場合は、新しく作成した string と argument として渡された string buffer を使って callback を呼び出します。
(sbcl-librarian::define-type :callback
:c-type "void*"
:alien-type (sb-alien:* (sb-alien:function sb-alien:void sb-alien:c-string (sb-alien:* sb-alien:char)))
:python-type "c_void_p")
(sbcl-librarian::define-type :char-buffer
:c-type "char*"
:alien-type (sb-alien:* sb-alien:char)
:python-type "c_char_p")
この section は、C、Python、Common Lisp における callback と char-buffer type を定義します。両者の C type と Python type は、それぞれ void* と char* です。callback の Common Lisp type は function prototype を指定します。つまり、void を返し、c-string と char への pointer という 2 つの parameter を取る function への pointer です。sb-alien:* は pointer を示すため、:callback は function への pointer です。:char-buffer type は、3 つの言語すべてで char* を表します。
この file の残りは、Hello World section で説明したものと似ています。
LISP Code を compile する
script.lisp は、Lisp source を compile し、wrapper code と Lisp core を output する単純な Lisp script です。
(require '#:asdf)
(asdf:load-system '#:libcallback)
(in-package #:sbcl-librarian/example/libcallback)
(build-bindings libcallback ".")
(build-python-bindings libcallback ".")
(build-core-and-die libcallback "." :compression t)
これで新しい file がいくつかできます。
libcallback.c は library の source code です。
#define CALLBACKING_API_BUILD
#include "libcallback.h"
void (*lisp_release_handle)(void* handle);
int (*lisp_handle_eq)(void* a, void* b);
void (*lisp_enable_debugger)();
void (*lisp_disable_debugger)();
void (*lisp_gc)();
err_t (*callback_call_callback)(void* fn, char* out_buffer);
extern int initialize_lisp(int argc, char **argv);
CALLBACKING_API int init(char* core) {
static int initialized = 0;
char *init_args[] = {"", "--core", core, "--noinform", };
if (initialized) return 1;
if (initialize_lisp(4, init_args) != 0) return -1;
initialized = 1;
return 0; }
先頭には、Lisp garbage collector に実行に適した時点であることを signal する lisp_gc など、SBCL 関連の function がいくつかあります。次に callback_call_callback function への pointer があります。最後に、Lisp code を実行する前に run すべき init function があります。
SBCL (version 2.4.2 時点) は Lisp core の de-initialize を support していなかったため、それを行う function はありません。
libcallback.h は、lispcallback.c と呼び出し側の任意の C code の両方で include されるべき header file です。これは lispcallback.c 内の function と function pointer の prototype、error enum、および bindings.lisp で追加された comment を含みます。
typedef enum { ERR_SUCCESS = 0, ERR_FAIL = 1, } err_t;
最後の file、lispcallback.py は library の Python wrapper です。最も注目すべき部分は次です。
from ctypes import *
from ctypes.util import find_library
try:
libpath = Path(find_library('libcallback')).resolve()
except TypeError as e:
raise Exception('Unable to locate libcallback') from e
file の残りは C header file と似ています。
この setup は compile 済み C library (shared object、DLL、dylib) を load し、その library に含まれる function と type を Python interpreter に知らせます。また、Python interpreter によって load されたときに Lisp core を initialize します。生成された library が C から呼び出される場合、initialization は手動で呼び出す必要があります。
C Code を compile する
cc -shared -fpic -o libcallback.so libcallback.c -L$SBCL_SRC/src/runtime -lsbcl
Mac OS では command が少し異なるかもしれません。
cc -dynamiclib -o libcallback.dylib libcallback.c -L$SBCL_SRC/src/runtime -lsbcl
$SBCL_SRC/src/runtime が $PATH にない場合は、$SBCL_SRC/src/runtime/libsbcl.so file を current directory に copy してください。
Run
すべて setup できたので、次の command で example code を実行できます。
$ python3 ./example.py
成功すれば、次の output が見えるはずです。
I guess it works!
Makefile
各 example には、Mac で build するために設計された Makefile が付属します。libsbcl.so library を自動的に build し、current directory へ copy することさえします。ただし、project (たとえば libcallback) を build する command は、Linux-based operating system と Windows (MSYS2 使用) で動くよう修正する必要があります。
CMake
CMake の利用は比較的 straightforward です。残念ながら、現在 CMake-aware library や vcpkg/conan package は存在しないため、必要な library を locate するには find_library で HINTS を使う必要があります。
my_project という project を compile し、LISP library を追加したいと仮定すると、次のように進められます。
# If there is a better way, let me know.
if(WIN32)
set(DIR_SEPARATOR ";")
else()
set(DIR_SEPARATOR ":")
endif()
# Set the ENV Vars for building the LISP part
set(SBCL_SRC "$ENV{SBCL_SRC}" CACHE PATH "Path to SBCL sources directory.")
set(SBCL_LIBRARIAN_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../sbcl-librarian" CACHE PATH "Source codes of SBCL-LIBRARIAN project.")
set(CL_SOURCE_REGISTRY "${CMAKE_CURRENT_SOURCE_DIR}${DIR_SEPARATOR}${SBCL_LIBRARIAN_DIR}" CACHE PATH "ASDF registry for building of the libray.")
# Find the SBCL library
find_library(libsbcl NAMES sbcl HINTS ${SBCL_SRC}/src/runtime/)
# Link the library to the C project
target_link_libraries(my_project ${libsbcl})
# Build LISP part of the project
add_custom_command(OUTPUT my_project-lisp.core my_project-lisp.c my_project-lisp.h my_project-lisp.py
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
COMMAND ${CMAKE_COMMAND} -E env CL_SOURCE_REGISTRY="${CL_SOURCE_REGISTRY}"
${SBCL_SRC}/run-sbcl.sh ARGS --script script.lisp
COMMAND ${CMAKE_COMMAND} -E copy_if_different my_project-lisp.core $<TARGET_FILE_DIR:my_project>
COMMAND ${CMAKE_COMMAND} -E copy_if_different my_project-lisp.c $<TARGET_FILE_DIR:my_project>
COMMAND ${CMAKE_COMMAND} -E copy_if_different my_project-lisp.h $<TARGET_FILE_DIR:my_project>
COMMAND ${CMAKE_COMMAND} -E copy_if_different my_project-lisp.py $<TARGET_FILE_DIR:my_project>
COMMAND ${CMAKE_COMMAND} -E rm my_project-lisp.core my_project-lisp.c my_project-lisp.h my_project-lisp.py
# Copy SBCL library if newer
add_custom_command(TARGET my_project POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
"${libsbcl}"
$<TARGET_FILE_DIR:my_project>)
これで SBCL-librarian を始めるための tutorial は終わりです。Common Lisp で作れるものについて、あなたの想像力が広がり、正しい方向へ進む助けになれば幸いです。
Page source: ja/dynamic-libraries.md