changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > org > notes / 20231105.org

changeset 12: 7d3ccfc2f7a0
parent: 4839b0675118
author: Richard Westhaver <ellis@rwest.io>
date: Fri, 16 Aug 2024 23:51:35 -0400
permissions: -rw-r--r--
description: keys
1 * DRAFT dylib-skel-1
2 :PROPERTIES:
3 :ID: b4d1bc91-f344-45fd-becc-cb20f00a3a61
4 :END:
5 - State "DRAFT" from [2023-11-05 Sun 22:23]
6 ** Overview
7 :PROPERTIES:
8 :ID: 2e490c4b-344e-4790-9184-1c05ba675f15
9 :END:
10 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
11 to rapidly develop high-quality software. As such, it's crucial that these two very
12 different languages (i.e. compilers) are able to interoperate seamlessly.
13 
14 Some interop methods are easy to accomodate via the OS - such as IPC or data sharing,
15 but others are a bit more difficult.
16 
17 In this 2-part series we'll build a FFI bridge between Rust and Lisp, which is something
18 that /can/ be difficult, due to some complications with Rust and because this is not the
19 most popular software stack (yet ;). This is an experiment and may not make it to our
20 code-base, but it's definitely something worth adding to the toolbox in case we need it.
21 
22 ** FFI
23 :PROPERTIES:
24 :ID: 985019fc-612a-44ab-b726-b9067432ad87
25 :END:
26 The level of interop we're after in this case is [[https://en.wikipedia.org/wiki/Foreign_function_interface][FFI]].
27 
28 Basically, calling Rust code from Lisp and vice-versa. There's an article about calling
29 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
30 for those interested.
31 *** Rust != C
32 :PROPERTIES:
33 :ID: 2f71a3c1-0b14-46a6-9d8d-f6ec697729cc
34 :END:
35 The complication(s) with Rust I mentioned early is really just that /it is not C/. =C=
36 is old, i.e. well-supported with a stable ABI, making the process of creating bindings
37 for a C library a breeze in many languages.
38 
39 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]]
40 of the Rustonomicon. Among other things it involves changing the calling-convention of
41 functions with a type signature and editing the Cargo.toml file to produce a
42 C-compatible ABI binary. The Rust default ABI is unstable and can't reliably be used
43 like the C ABI can.
44 
45 *** Overhead
46 :PROPERTIES:
47 :ID: 4ea79f68-55ec-4da3-a184-8343d49532b6
48 :END:
49 Using FFI involves some overhead. Check [[https://github.com/dyu/ffi-overhead][here]] for an example benchmark across a few
50 languages. While building the NAS-T core, I'm very much aware of this, and will need a
51 few sanity benchmarks to make sure the cost doesn't outweigh the benefit. In particular,
52 I'm concerned about crossing multiple language barriers (Rust<->C<->Lisp).
53 
54 ** Rust -> C -> Lisp
55 :PROPERTIES:
56 :ID: a498276c-8525-4a43-aa40-4b05f76a29a9
57 :END:
58 *** Setup
59 :PROPERTIES:
60 :ID: 19f96ef7-af92-496e-9d42-70c4d4c85051
61 :END:
62 For starters, I'm going to assume we all have Rust (via =rustup=) and Lisp (=sbcl= only)
63 installed on our GNU/Linux system (some tweaks needed for Darwin/Windows, not covered in
64 this post).
65 **** Cargo
66 :PROPERTIES:
67 :ID: c929e0b6-b6f2-4383-9412-1610329ab28c
68 :END:
69 Create a new library crate. For this example we're focusing on a 'skeleton' for
70 /dynamic/ libraries only, so our experiment will be called =dylib-skel= or *dysk* for
71 short.
72 src_sh[:exports code]{cargo init dysk --lib && cd dysk}
73 
74 A =src/lib.rs= will be generated for you. Go ahead and delete that. We're going to be
75 making our own =lib.rs= file directly in the root directory (just to be cool).
76 
77 The next step is to edit your =Cargo.toml= file. Add these lines after the =[package]=
78 section and before =[dependencies]=:
79 #+begin_src conf-toml
80 [lib]
81 crate-type = ["cdylib","rlib"]
82 path = "lib.rs"
83 [[bin]]
84 name="dysk-test"
85 path="test.rs"
86 #+end_src
87 
88 This tells Rust to generate a shared C-compatible object with a =.so= extension which we
89 can open using [[https://man.archlinux.org/man/dlopen.3.en][dlopen]].
90 **** cbindgen
91 :PROPERTIES:
92 :ID: 256ac288-c5a0-473a-ab65-2d6503bd423c
93 :END:
94 ***** install
95 :PROPERTIES:
96 :ID: fc476f64-6b68-417a-8540-ca23ce27fa25
97 :END:
98 Next, we want the =cbindgen= program which we'll use to generate header files for
99 C/C++. This step isn't necessary at all, we just want it for further experimentation.
100 
101 src_sh[:exports code]{cargo install --force cbindgen}
102 
103 We append the =cbindgen= crate as a /build dependency/ to our =Cargo.toml= like so:
104 #+begin_src conf-toml
105 [build-dependencies]
106 cbindgen = "0.24"
107 #+end_src
108 ***** cbindgen.toml
109 :PROPERTIES:
110 :ID: 111e27f7-0b9c-4eef-9117-f7c8ba3f511c
111 :END:
112 #+begin_src conf-toml :tangle cbindgen.toml
113 language = "C"
114 autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
115 include_version = true
116 namespace = "dysk"
117 cpp_compat = true
118 after_includes = "#define DYSK_VERSION \"0.1.0\""
119 line_length = 88
120 tab_width = 2
121 documentation = true
122 documentation_style = "c99"
123 usize_is_size_t = true
124 [cython]
125 header = '"dysk.h"'
126 #+end_src
127 ***** build.rs
128 :PROPERTIES:
129 :ID: 9fc271b2-9acb-4f4b-aa61-82d60d2ddb9e
130 :END:
131 #+begin_src rust :tangle build.rs
132 fn main() -> Result<(), cbindgen::Error> {
133  if let Ok(b) = cbindgen::generate(std::env::var("CARGO_MANIFEST_DIR").unwrap()) {
134  b.write_to_file("dysk.h"); Ok(())}
135  else { panic!("failed to generate dysk.h from cbindgen.toml") } }
136 #+end_src
137 *** lib.rs
138 :PROPERTIES:
139 :ID: 6b524921-2ae0-43f0-bb85-d9955b0e689c
140 :END:
141 #+begin_src rust :tangle lib.rs
142 //! lib.rs --- dysk library
143 use std::ffi::{c_char, c_int, CString};
144 #[no_mangle]
145 pub extern "C" fn dysk_hello() -> *const c_char {
146  CString::new("hello from rust").unwrap().into_raw()}
147 #[no_mangle]
148 pub extern "C" fn dysk_plus(a:c_int,b:c_int) -> c_int {a+b}
149 #[no_mangle]
150 pub extern "C" fn dysk_plus1(n:c_int) -> c_int {n+1}
151 #+end_src
152 *** test.rs
153 :PROPERTIES:
154 :ID: cc7c6538-33a6-40c6-94ef-2a9c259c975a
155 :END:
156 #+begin_src rust :tangle test.rs
157 //! test.rs --- dysk test
158 fn main() { let mut i = 0u32; while i < 500000000 {i+=1; dysk::dysk_plus1(2 as core::ffi::c_int);}}
159 #+end_src
160 *** compile
161 :PROPERTIES:
162 :ID: 337a24d1-f305-4e1a-9052-47a53591cb2f
163 :END:
164 #+begin_src sh
165 cargo build --release
166 #+end_src
167 *** load from SBCL
168 :PROPERTIES:
169 :ID: a4813269-92fb-4f52-aef0-3a36dce3cf69
170 :END:
171 #+begin_src lisp :tangle dysk.lisp
172 (load-shared-object #P"target/release/libdysk.so")
173 (define-alien-routine dysk-hello c-string)
174 (define-alien-routine dysk-plus int (a int) (b int))
175 (define-alien-routine dysk-plus1 int (n int))
176 (dysk-hello) ;; => "hello from rust"
177 #+end_src
178 *** benchmark
179 :PROPERTIES:
180 :ID: 1a8ca441-f290-46c7-b979-1e7e0d1d063b
181 :END:
182 #+begin_src shell
183 time target/release/dysk-test
184 #+end_src
185 #+begin_src lisp :tangle test.lisp
186 (time (dotimes (_ 500000000) (dysk-plus1 2)))
187 #+end_src