18
|
1
|
#+title: Shared Library Skeletons |
20
|
2
|
#+setupfile: ../../../clean.theme |
18
|
3
|
* Overview |
19
|
4
|
:PROPERTIES: |
|
5
|
:ID: 748ba6e4-60db-4ff7-9d78-12c3f67644d8 |
|
6
|
:END: |
18
|
7
|
+ CODE :: [[https://lab.rwest.io/packy/stash/dysk][packy/stash/dysk]] |
|
8
|
Our core languages are [[https://www.rust-lang.org/][Rust]] and [[https://lisp-lang.org/][Lisp]] - this is the killer combo which will allow NAS-T |
|
9
|
to rapidly develop high-quality software. As such, it's crucial that these two very |
|
10
|
different languages (i.e. compilers) are able to interoperate seamlessly. |
|
11
|
|
|
12
|
Some interop methods are easy to accomodate via the OS - such as IPC or data sharing, |
|
13
|
but others are a bit more difficult. |
|
14
|
|
|
15
|
In this 2-part series we'll build a FFI bridge between Rust and Lisp, which is something |
|
16
|
that /can/ be difficult, due to some complications with Rust and because this is not the |
|
17
|
most popular software stack (yet ;). This is an experiment and may not make it to our |
|
18
|
code-base, but it's definitely something worth adding to the toolbox in case we need it. |
|
19
|
** FFI |
19
|
20
|
:PROPERTIES: |
|
21
|
:ID: 0e490b3b-0c15-4963-9d43-7d2a689ec371 |
|
22
|
:END: |
18
|
23
|
The level of interop we're after in this case is [[https://en.wikipedia.org/wiki/Foreign_function_interface][FFI]]. |
|
24
|
|
|
25
|
Basically, calling Rust code from Lisp and vice-versa. There's an article about calling |
|
26
|
Rust from Common Lisp [[https://dev.to/veer66/calling-rust-from-common-lisp-45c5][here]] which shows the basics and serves as a great starting point |
|
27
|
for those interested. |
|
28
|
*** Rust ABI |
19
|
29
|
:PROPERTIES: |
|
30
|
:ID: 6c24dc95-eea6-44fb-8c7f-bb49227ca7bf |
|
31
|
:END: |
18
|
32
|
The complication(s) with Rust I mentioned early is really just that /it is not C/. =C= |
|
33
|
is old, i.e. well-supported with a stable ABI, making the process of creating bindings |
|
34
|
for a C library a breeze in many languages. |
|
35
|
|
|
36
|
For a Rust library we need to first appease the compiler, as explained in [[https://doc.rust-lang.org/nomicon/ffi.html#calling-rust-code-from-c][this section]] |
|
37
|
of the Rustonomicon. Among other things it involves changing the calling-convention of |
|
38
|
functions with a type signature and editing the Cargo.toml file to produce a |
|
39
|
C-compatible ABI binary. The Rust default ABI is unstable and can't reliably be used |
|
40
|
like the C ABI can. |
|
41
|
|
|
42
|
[[https://github.com/rodrimati1992/abi_stable_crates][abi_stable_crates]] is a project which addresses some of the ABI concerns, presenting a |
|
43
|
sort of ABI-API as a Rust library. Perhaps this is the direction the ecosystem will go |
|
44
|
with in order to maintain an unstable ABI, but for now there is no 'clear' pathway for a |
|
45
|
friction-less FFI development experience in Rust. |
|
46
|
|
|
47
|
*** Overhead |
19
|
48
|
:PROPERTIES: |
|
49
|
:ID: faf8fbcb-3403-4e2f-b881-ef1404ad9780 |
|
50
|
:END: |
18
|
51
|
Using FFI involves some overhead. Check [[https://github.com/dyu/ffi-overhead][here]] for an example benchmark across a few |
|
52
|
languages. While building the NAS-T core, I'm very much aware of this, and will need a |
|
53
|
few sanity benchmarks to make sure the cost doesn't outweigh the benefit. In particular, |
|
54
|
I'm concerned about crossing multiple language barriers (Rust<->C<->Lisp). |
|
55
|
|
|
56
|
* basic example |
19
|
57
|
:PROPERTIES: |
|
58
|
:ID: ab04fff7-51c9-4f1c-b61b-fa4739932343 |
|
59
|
:END: |
18
|
60
|
** Setup |
19
|
61
|
:PROPERTIES: |
|
62
|
:ID: 08420b18-3856-4b62-9523-98fb3398afca |
|
63
|
:END: |
18
|
64
|
For starters, I'm going to assume we all have Rust (via [[https://rustup.rs/][rustup]]) and Lisp ([[https://www.sbcl.org/][sbcl]] only) |
|
65
|
installed on our GNU/Linux system (some tweaks needed for Darwin/Windows, not covered in |
|
66
|
this post). |
|
67
|
*** Cargo |
19
|
68
|
:PROPERTIES: |
|
69
|
:ID: cfeb1299-67c5-4a64-863e-f214e195e176 |
|
70
|
:END: |
18
|
71
|
Create a new library crate. For this example we're focusing on a 'skeleton' for |
|
72
|
/dynamic/ libraries only, so our experiment will be called =dylib-skel= or *dysk* for |
|
73
|
short. |
|
74
|
src_sh[:exports code]{cargo init dysk --lib && cd dysk} |
|
75
|
|
|
76
|
A =src/lib.rs= will be generated for you. Go ahead and delete that. We're going to be |
|
77
|
making our own =lib.rs= file directly in the root directory (just to be cool). |
|
78
|
|
|
79
|
The next step is to edit your =Cargo.toml= file. Add these lines after the =[package]= |
|
80
|
section and before =[dependencies]=: |
|
81
|
#+begin_src conf-toml |
|
82
|
[lib] |
|
83
|
crate-type = ["cdylib","rlib"] |
|
84
|
path = "lib.rs" |
|
85
|
[[bin]] |
|
86
|
name="dysk-test" |
|
87
|
path="test.rs" |
|
88
|
#+end_src |
|
89
|
|
|
90
|
This tells Rust to generate a shared C-compatible object with a =.so= extension which we |
|
91
|
can open using [[https://man.archlinux.org/man/dlopen.3.en][dlopen]]. |
|
92
|
*** cbindgen |
19
|
93
|
:PROPERTIES: |
|
94
|
:ID: 435d84d6-c959-4bf6-ad37-fb6e524b54ff |
|
95
|
:END: |
18
|
96
|
**** install |
19
|
97
|
:PROPERTIES: |
|
98
|
:ID: 15115ab8-e7b4-4050-9567-bf026cc140d1 |
|
99
|
:END: |
18
|
100
|
Next, we want the =cbindgen= program which we'll use to generate header files for |
|
101
|
C/C++. This step isn't necessary at all, we just want it for further experimentation. |
|
102
|
|
|
103
|
src_sh[:exports code]{cargo install --force cbindgen} |
|
104
|
|
|
105
|
We append the =cbindgen= crate as a /build dependency/ to our =Cargo.toml= like so: |
|
106
|
#+begin_src conf-toml |
|
107
|
[build-dependencies] |
|
108
|
cbindgen = "0.24" |
|
109
|
#+end_src |
|
110
|
**** cbindgen.toml |
19
|
111
|
:PROPERTIES: |
|
112
|
:ID: 21d776b7-9f8d-445c-83c2-8e0c28751827 |
|
113
|
:END: |
18
|
114
|
#+begin_src conf-toml :tangle cbindgen.toml |
|
115
|
language = "C" |
|
116
|
autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */" |
|
117
|
include_version = true |
|
118
|
namespace = "dysk" |
|
119
|
cpp_compat = true |
|
120
|
after_includes = "#define DYSK_VERSION \"0.1.0\"" |
|
121
|
line_length = 88 |
|
122
|
tab_width = 2 |
|
123
|
documentation = true |
|
124
|
documentation_style = "c99" |
|
125
|
usize_is_size_t = true |
|
126
|
[cython] |
|
127
|
header = "dysk.h" |
|
128
|
#+end_src |
|
129
|
**** build.rs |
19
|
130
|
:PROPERTIES: |
|
131
|
:ID: 469dd142-bafa-4076-8ba9-baf088e60260 |
|
132
|
:END: |
18
|
133
|
#+begin_src rust :tangle build.rs |
|
134
|
fn main() -> Result<(), cbindgen::Error> { |
|
135
|
if let Ok(b) = cbindgen::generate(std::env::var("CARGO_MANIFEST_DIR").unwrap()) { |
|
136
|
b.write_to_file("dysk.h"); Ok(())} |
|
137
|
else { panic!("failed to generate dysk.h from cbindgen.toml") } } |
|
138
|
#+end_src |
|
139
|
** lib.rs |
19
|
140
|
:PROPERTIES: |
|
141
|
:ID: e9d50e77-4d1e-4d38-90aa-9977d8c6c888 |
|
142
|
:END: |
18
|
143
|
#+begin_src rust :tangle lib.rs |
|
144
|
//! lib.rs --- dysk library |
|
145
|
use std::ffi::{c_char, c_int, CString}; |
|
146
|
#[no_mangle] |
|
147
|
pub extern "C" fn hello() -> *const c_char { |
|
148
|
CString::new("hello from rust").unwrap().into_raw()} |
|
149
|
#[no_mangle] |
|
150
|
pub extern "C" fn plus(a:c_int,b:c_int) -> c_int {a+b} |
|
151
|
#[no_mangle] |
|
152
|
pub extern "C" fn plus1(n:c_int) -> c_int {n+1} |
|
153
|
#+end_src |
|
154
|
** test.rs |
19
|
155
|
:PROPERTIES: |
|
156
|
:ID: 84b5dffe-7171-4d72-9788-f288e3185070 |
|
157
|
:END: |
18
|
158
|
#+begin_src rust :tangle test.rs |
|
159
|
//! test.rs --- dysk test |
|
160
|
fn main() { let mut i = 0u32; while i < 500000000 {i+=1; dysk::plus1(2 as core::ffi::c_int);}} |
|
161
|
#+end_src |
|
162
|
** compile |
19
|
163
|
:PROPERTIES: |
|
164
|
:ID: 30f4fb3e-0757-4df6-947f-8d1e11edabf9 |
|
165
|
:END: |
18
|
166
|
#+begin_src sh |
|
167
|
cargo build --release |
|
168
|
#+end_src |
|
169
|
** load from SBCL |
19
|
170
|
:PROPERTIES: |
|
171
|
:ID: 69b517f2-648d-4e44-b956-7879b3dadf99 |
|
172
|
:END: |
18
|
173
|
#+begin_src lisp :tangle dysk.lisp |
|
174
|
;;; dysk.lisp |
|
175
|
;; (dysk:hello) ;; => "hello from rust" |
|
176
|
(defpackage :dysk |
|
177
|
(:use :cl :sb-alien) |
|
178
|
(:export :hello :plus :plus1)) |
|
179
|
(in-package :dysk) |
|
180
|
(load-shared-object #P"target/release/libdysk.so") |
|
181
|
(define-alien-routine hello c-string) |
|
182
|
(define-alien-routine plus int (a int) (b int)) |
|
183
|
(define-alien-routine plus1 int (n int)) |
|
184
|
#+end_src |
|
185
|
** benchmark |
19
|
186
|
:PROPERTIES: |
|
187
|
:ID: 86e4195d-601b-4736-b852-1209a6d38bdf |
|
188
|
:END: |
18
|
189
|
#+begin_src shell |
|
190
|
time target/release/dysk-test |
|
191
|
#+end_src |
|
192
|
#+begin_src lisp :tangle test.lisp |
|
193
|
(time (dotimes (_ 500000000) (dysk:plus1 2))) |
|
194
|
#+end_src |
|
195
|
|