changelog shortlog graph tags branches changeset file revisions annotate raw help

Mercurial > org > notes / 20231105.org

revision 15: 45ac54093c09
parent 14: a04ca5a66178
child 16: a63dfd1affed
     1.1--- a/20231105.org	Tue Aug 27 21:35:44 2024 -0400
     1.2+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.3@@ -1,187 +0,0 @@
     1.4-* DRAFT dylib-skel-1
     1.5-:PROPERTIES:
     1.6-:ID:       b4d1bc91-f344-45fd-becc-cb20f00a3a61
     1.7-:END:
     1.8-- State "DRAFT"      from              [2023-11-05 Sun 22:23]
     1.9-** Overview
    1.10-:PROPERTIES:
    1.11-:ID:       2e490c4b-344e-4790-9184-1c05ba675f15
    1.12-:END:
    1.13-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
    1.14-to rapidly develop high-quality software. As such, it's crucial that these two very
    1.15-different languages (i.e. compilers) are able to interoperate seamlessly.
    1.16-
    1.17-Some interop methods are easy to accomodate via the OS - such as IPC or data sharing,
    1.18-but others are a bit more difficult.
    1.19-
    1.20-In this 2-part series we'll build a FFI bridge between Rust and Lisp, which is something
    1.21-that /can/ be difficult, due to some complications with Rust and because this is not the
    1.22-most popular software stack (yet ;). This is an experiment and may not make it to our
    1.23-code-base, but it's definitely something worth adding to the toolbox in case we need it.
    1.24-
    1.25-** FFI
    1.26-:PROPERTIES:
    1.27-:ID:       985019fc-612a-44ab-b726-b9067432ad87
    1.28-:END:
    1.29-The level of interop we're after in this case is [[https://en.wikipedia.org/wiki/Foreign_function_interface][FFI]].
    1.30-
    1.31-Basically, calling Rust code from Lisp and vice-versa. There's an article about calling
    1.32-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
    1.33-for those interested.
    1.34-*** Rust != C
    1.35-:PROPERTIES:
    1.36-:ID:       2f71a3c1-0b14-46a6-9d8d-f6ec697729cc
    1.37-:END:
    1.38-The complication(s) with Rust I mentioned early is really just that /it is not C/. =C=
    1.39-is old, i.e. well-supported with a stable ABI, making the process of creating bindings
    1.40-for a C library a breeze in many languages.
    1.41-
    1.42-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]]
    1.43-of the Rustonomicon. Among other things it involves changing the calling-convention of
    1.44-functions with a type signature and editing the Cargo.toml file to produce a
    1.45-C-compatible ABI binary. The Rust default ABI is unstable and can't reliably be used
    1.46-like the C ABI can.
    1.47-
    1.48-*** Overhead
    1.49-:PROPERTIES:
    1.50-:ID:       4ea79f68-55ec-4da3-a184-8343d49532b6
    1.51-:END:
    1.52-Using FFI involves some overhead. Check [[https://github.com/dyu/ffi-overhead][here]] for an example benchmark across a few
    1.53-languages. While building the NAS-T core, I'm very much aware of this, and will need a
    1.54-few sanity benchmarks to make sure the cost doesn't outweigh the benefit. In particular,
    1.55-I'm concerned about crossing multiple language barriers (Rust<->C<->Lisp).
    1.56-
    1.57-** Rust -> C -> Lisp
    1.58-:PROPERTIES:
    1.59-:ID:       a498276c-8525-4a43-aa40-4b05f76a29a9
    1.60-:END:
    1.61-*** Setup
    1.62-:PROPERTIES:
    1.63-:ID:       19f96ef7-af92-496e-9d42-70c4d4c85051
    1.64-:END:
    1.65-For starters, I'm going to assume we all have Rust (via =rustup=) and Lisp (=sbcl= only)
    1.66-installed on our GNU/Linux system (some tweaks needed for Darwin/Windows, not covered in
    1.67-this post).
    1.68-**** Cargo
    1.69-:PROPERTIES:
    1.70-:ID:       c929e0b6-b6f2-4383-9412-1610329ab28c
    1.71-:END:
    1.72-Create a new library crate. For this example we're focusing on a 'skeleton' for
    1.73-/dynamic/ libraries only, so our experiment will be called =dylib-skel= or *dysk* for
    1.74-short.
    1.75-src_sh[:exports code]{cargo init dysk --lib && cd dysk} 
    1.76-
    1.77-A =src/lib.rs= will be generated for you. Go ahead and delete that. We're going to be
    1.78-making our own =lib.rs= file directly in the root directory (just to be cool).
    1.79-
    1.80-The next step is to edit your =Cargo.toml= file. Add these lines after the =[package]=
    1.81-section and before =[dependencies]=:
    1.82-#+begin_src conf-toml
    1.83-[lib]
    1.84-crate-type = ["cdylib","rlib"]
    1.85-path = "lib.rs"
    1.86-[[bin]]
    1.87-name="dysk-test"
    1.88-path="test.rs"
    1.89-#+end_src
    1.90-
    1.91-This tells Rust to generate a shared C-compatible object with a =.so= extension which we
    1.92-can open using [[https://man.archlinux.org/man/dlopen.3.en][dlopen]].
    1.93-**** cbindgen
    1.94-:PROPERTIES:
    1.95-:ID:       256ac288-c5a0-473a-ab65-2d6503bd423c
    1.96-:END:
    1.97-***** install
    1.98-:PROPERTIES:
    1.99-:ID:       fc476f64-6b68-417a-8540-ca23ce27fa25
   1.100-:END:
   1.101-Next, we want the =cbindgen= program which we'll use to generate header files for
   1.102-C/C++. This step isn't necessary at all, we just want it for further experimentation.
   1.103-
   1.104-src_sh[:exports code]{cargo install --force cbindgen}
   1.105-
   1.106-We append the =cbindgen= crate as a /build dependency/ to our =Cargo.toml= like so:
   1.107-#+begin_src conf-toml
   1.108-[build-dependencies]
   1.109-cbindgen = "0.24"
   1.110-#+end_src
   1.111-***** cbindgen.toml
   1.112-:PROPERTIES:
   1.113-:ID:       111e27f7-0b9c-4eef-9117-f7c8ba3f511c
   1.114-:END:
   1.115-#+begin_src conf-toml :tangle cbindgen.toml
   1.116-language = "C"
   1.117-autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
   1.118-include_version = true
   1.119-namespace = "dysk"
   1.120-cpp_compat = true
   1.121-after_includes = "#define DYSK_VERSION \"0.1.0\""
   1.122-line_length = 88
   1.123-tab_width = 2
   1.124-documentation = true
   1.125-documentation_style = "c99"
   1.126-usize_is_size_t = true
   1.127-[cython]
   1.128-header = '"dysk.h"'
   1.129-#+end_src
   1.130-***** build.rs
   1.131-:PROPERTIES:
   1.132-:ID:       9fc271b2-9acb-4f4b-aa61-82d60d2ddb9e
   1.133-:END:
   1.134-#+begin_src rust :tangle build.rs
   1.135-fn main() -> Result<(), cbindgen::Error> {
   1.136-  if let Ok(b) = cbindgen::generate(std::env::var("CARGO_MANIFEST_DIR").unwrap()) {
   1.137-    b.write_to_file("dysk.h"); Ok(())}
   1.138-  else { panic!("failed to generate dysk.h from cbindgen.toml") } }
   1.139-#+end_src
   1.140-*** lib.rs
   1.141-:PROPERTIES:
   1.142-:ID:       6b524921-2ae0-43f0-bb85-d9955b0e689c
   1.143-:END:
   1.144-#+begin_src rust :tangle lib.rs
   1.145-//! lib.rs --- dysk library
   1.146-use std::ffi::{c_char, c_int, CString};
   1.147-#[no_mangle]
   1.148-pub extern "C" fn dysk_hello() -> *const c_char {
   1.149-  CString::new("hello from rust").unwrap().into_raw()}
   1.150-#[no_mangle]
   1.151-pub extern "C" fn dysk_plus(a:c_int,b:c_int) -> c_int {a+b}
   1.152-#[no_mangle]
   1.153-pub extern "C" fn dysk_plus1(n:c_int) -> c_int {n+1}
   1.154-#+end_src
   1.155-*** test.rs
   1.156-:PROPERTIES:
   1.157-:ID:       cc7c6538-33a6-40c6-94ef-2a9c259c975a
   1.158-:END:
   1.159-#+begin_src rust :tangle test.rs
   1.160-//! test.rs --- dysk test
   1.161-fn main() { let mut i = 0u32; while i < 500000000 {i+=1; dysk::dysk_plus1(2 as core::ffi::c_int);}}
   1.162-#+end_src
   1.163-*** compile
   1.164-:PROPERTIES:
   1.165-:ID:       337a24d1-f305-4e1a-9052-47a53591cb2f
   1.166-:END:
   1.167-#+begin_src sh
   1.168-cargo build --release
   1.169-#+end_src
   1.170-*** load from SBCL
   1.171-:PROPERTIES:
   1.172-:ID:       a4813269-92fb-4f52-aef0-3a36dce3cf69
   1.173-:END:
   1.174-#+begin_src lisp :tangle dysk.lisp
   1.175-(load-shared-object #P"target/release/libdysk.so")
   1.176-(define-alien-routine dysk-hello c-string)
   1.177-(define-alien-routine dysk-plus int (a int) (b int))
   1.178-(define-alien-routine dysk-plus1 int (n int))
   1.179-(dysk-hello) ;; => "hello from rust"
   1.180-#+end_src
   1.181-*** benchmark
   1.182-:PROPERTIES:
   1.183-:ID:       1a8ca441-f290-46c7-b979-1e7e0d1d063b
   1.184-:END:
   1.185-#+begin_src shell
   1.186-time target/release/dysk-test
   1.187-#+end_src
   1.188-#+begin_src lisp :tangle test.lisp
   1.189-(time (dotimes (_ 500000000) (dysk-plus1 2)))
   1.190-#+end_src