changelog shortlog graph tags branches changeset files revisions annotate raw help

Mercurial > org > blog / draft/dylib-skel.org

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