changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > org > notes / 20231105.org

changeset 2: 04e86b94ef1a
parent: 87b04952fb18
child: 812feca5a874
author: Richard Westhaver <ellis@rwest.io>
date: Thu, 06 Jun 2024 23:16:37 -0400
permissions: -rw-r--r--
description: style update
1 #+setupfile: ../clean.theme
2 * DRAFT dylib-skel-1
3 - State "DRAFT" from [2023-11-05 Sun 22:23]
4 ** Overview
5 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
6 to rapidly develop high-quality software. As such, it's crucial that these two very
7 different languages (i.e. compilers) are able to interoperate seamlessly.
8 
9 Some interop methods are easy to accomodate via the OS - such as IPC or data sharing,
10 but others are a bit more difficult.
11 
12 In this 2-part series we'll build a FFI bridge between Rust and Lisp, which is something
13 that /can/ be difficult, due to some complications with Rust and because this is not the
14 most popular software stack (yet ;). This is an experiment and may not make it to our
15 code-base, but it's definitely something worth adding to the toolbox in case we need it.
16 
17 ** FFI
18 The level of interop we're after in this case is [[https://en.wikipedia.org/wiki/Foreign_function_interface][FFI]].
19 
20 Basically, calling Rust code from Lisp and vice-versa. There's an article about calling
21 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
22 for those interested.
23 *** Rust != C
24 The complication(s) with Rust I mentioned early is really just that /it is not C/. =C=
25 is old, i.e. well-supported with a stable ABI, making the process of creating bindings
26 for a C library a breeze in many languages.
27 
28 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]]
29 of the Rustonomicon. Among other things it involves changing the calling-convention of
30 functions with a type signature and editing the Cargo.toml file to produce a
31 C-compatible ABI binary. The Rust default ABI is unstable and can't reliably be used
32 like the C ABI can.
33 
34 *** Overhead
35 Using FFI involves some overhead. Check [[https://github.com/dyu/ffi-overhead][here]] for an example benchmark across a few
36 languages. While building the NAS-T core, I'm very much aware of this, and will need a
37 few sanity benchmarks to make sure the cost doesn't outweigh the benefit. In particular,
38 I'm concerned about crossing multiple language barriers (Rust<->C<->Lisp).
39 
40 ** Rust -> C -> Lisp
41 *** Setup
42 For starters, I'm going to assume we all have Rust (via =rustup=) and Lisp (=sbcl= only)
43 installed on our GNU/Linux system (some tweaks needed for Darwin/Windows, not covered in
44 this post).
45 **** Cargo
46 Create a new library crate. For this example we're focusing on a 'skeleton' for
47 /dynamic/ libraries only, so our experiment will be called =dylib-skel= or *dysk* for
48 short.
49 src_sh[:exports code]{cargo init dysk --lib && cd dysk}
50 
51 A =src/lib.rs= will be generated for you. Go ahead and delete that. We're going to be
52 making our own =lib.rs= file directly in the root directory (just to be cool).
53 
54 The next step is to edit your =Cargo.toml= file. Add these lines after the =[package]=
55 section and before =[dependencies]=:
56 #+begin_src conf-toml
57 [lib]
58 crate-type = ["cdylib","rlib"]
59 path = "lib.rs"
60 [[bin]]
61 name="dysk-test"
62 path="test.rs"
63 #+end_src
64 
65 This tells Rust to generate a shared C-compatible object with a =.so= extension which we
66 can open using [[https://man.archlinux.org/man/dlopen.3.en][dlopen]].
67 **** cbindgen
68 ***** install
69 Next, we want the =cbindgen= program which we'll use to generate header files for
70 C/C++. This step isn't necessary at all, we just want it for further experimentation.
71 
72 src_sh[:exports code]{cargo install --force cbindgen}
73 
74 We append the =cbindgen= crate as a /build dependency/ to our =Cargo.toml= like so:
75 #+begin_src conf-toml
76 [build-dependencies]
77 cbindgen = "0.24"
78 #+end_src
79 ***** cbindgen.toml
80 #+begin_src conf-toml :tangle cbindgen.toml
81 language = "C"
82 autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
83 include_version = true
84 namespace = "dysk"
85 cpp_compat = true
86 after_includes = "#define DYSK_VERSION \"0.1.0\""
87 line_length = 88
88 tab_width = 2
89 documentation = true
90 documentation_style = "c99"
91 usize_is_size_t = true
92 [cython]
93 header = '"dysk.h"'
94 #+end_src
95 ***** build.rs
96 #+begin_src rust :tangle build.rs
97 fn main() -> Result<(), cbindgen::Error> {
98  if let Ok(b) = cbindgen::generate(std::env::var("CARGO_MANIFEST_DIR").unwrap()) {
99  b.write_to_file("dysk.h"); Ok(())}
100  else { panic!("failed to generate dysk.h from cbindgen.toml") } }
101 #+end_src
102 *** lib.rs
103 #+begin_src rust :tangle lib.rs
104 //! lib.rs --- dysk library
105 use std::ffi::{c_char, c_int, CString};
106 #[no_mangle]
107 pub extern "C" fn dysk_hello() -> *const c_char {
108  CString::new("hello from rust").unwrap().into_raw()}
109 #[no_mangle]
110 pub extern "C" fn dysk_plus(a:c_int,b:c_int) -> c_int {a+b}
111 #[no_mangle]
112 pub extern "C" fn dysk_plus1(n:c_int) -> c_int {n+1}
113 #+end_src
114 *** test.rs
115 #+begin_src rust :tangle test.rs
116 //! test.rs --- dysk test
117 fn main() { let mut i = 0u32; while i < 500000000 {i+=1; dysk::dysk_plus1(2 as core::ffi::c_int);}}
118 #+end_src
119 *** compile
120 #+begin_src sh
121 cargo build --release
122 #+end_src
123 *** load from SBCL
124 #+begin_src lisp :tangle dysk.lisp
125 (load-shared-object #P"target/release/libdysk.so")
126 (define-alien-routine dysk-hello c-string)
127 (define-alien-routine dysk-plus int (a int) (b int))
128 (define-alien-routine dysk-plus1 int (n int))
129 (dysk-hello) ;; => "hello from rust"
130 #+end_src
131 *** benchmark
132 #+begin_src shell
133 time target/release/dysk-test
134 #+end_src
135 #+begin_src lisp :tangle test.lisp
136 (time (dotimes (_ 500000000) (dysk-plus1 2)))
137 #+end_src