changelog shortlog graph tags branches changeset files revisions annotate raw help

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

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