diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1b73a4fe..065edfc0 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -107,25 +107,6 @@ jobs: cargo fuzz run --fuzz-dir fuzz "$target" -- -runs=1 done - ExternalJetLib: - name: External Jet Library - runs-on: ubuntu-latest - steps: - - name: "Checkout repo" - uses: actions/checkout@v4 - - - name: "Setup cargo-rbmt" - uses: ./.github/actions/setup-rbmt - - - name: "Build external jet dylib" - run: cargo rbmt --lock-file existing run --toolchain stable -- build --manifest-path external-jet-lib-example/Cargo.toml --lib - - - name: "Build external-lib-consumer binary" - run: cargo rbmt --lock-file existing run --toolchain stable -- build --manifest-path external-jet-lib-example/Cargo.toml --bin external-lib-consumer - - - name: "Run external-lib-consumer with compiled dylib" - run: ./target/debug/external-lib-consumer ./target/debug/libexternal_jet_lib_example.so - IntegrationBuild: name: Compile daemon integration tests - ${{ matrix.os }} runs-on: ${{ matrix.os }} diff --git a/Cargo.lock b/Cargo.lock index 4ab10507..2c0d8f02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -417,13 +417,6 @@ dependencies = [ "windows-sys 0.59.0", ] -[[package]] -name = "external-jet-lib-example" -version = "0.1.0" -dependencies = [ - "simplicityhl", -] - [[package]] name = "fastrand" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 0de3cbfd..8f977160 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,6 @@ required-features = ["docs", "serde"] default = [ "serde" ] serde = ["dep:serde", "dep:serde_json"] docs = [] -external-jets = [] fmt = [] [dependencies] @@ -53,7 +52,7 @@ ariadne = "=0.5.0" getrandom = { version = "0.2", features = ["js"] } [workspace] -members = ["codegen", "fuzz", "external-jet-lib-example", "bitcoind-tests"] +members = ["codegen", "fuzz", "bitcoind-tests"] [workspace.metadata.rbmt.toolchains] nightly = "nightly-2025-08-23" diff --git a/external-jet-lib-example/Cargo.toml b/external-jet-lib-example/Cargo.toml deleted file mode 100644 index 42bc09e7..00000000 --- a/external-jet-lib-example/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -name = "external-jet-lib-example" -version = "0.1.0" -edition = "2021" -rust-version = "1.79.0" - -[package.metadata.rbmt.lint] -# Bitcoin/Elements use different hash and hex majors, serde/wasm use different syn majors. -allowed_duplicates = ["bitcoin_hashes", "hex-conservative", "syn"] - -[[bin]] -name = "external-lib-consumer" -path = "src/main.rs" - -[lib] -crate-type = ["cdylib"] - -[dependencies] -simplicityhl = { path = "..", features = ["external-jets"] } diff --git a/external-jet-lib-example/README.md b/external-jet-lib-example/README.md deleted file mode 100644 index a9b34559..00000000 --- a/external-jet-lib-example/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# External Jet Loading - -SimplicityHL supports loading custom jet sets at runtime from a shared library (`.so` / `.dylib` / `.dll`). This crate is an end-to-end example of how to build such a library and consume it. - -## How It Works - -### 1. Enable the feature flag - -The external jet machinery lives behind the `external-jets` Cargo feature in the main `simplicityhl` crate. Both the library and its consumer must opt in: - -```toml -[dependencies] -simplicityhl = { path = "..", features = ["external-jets"] } -``` - -### 2. Build a `cdylib` that exports the required symbols - -See [src/lib.rs](src/lib.rs) and [src/jet.rs](src/jet.rs) for a minimal implementation with a single `verify` jet. - -### 3. Initialize the library at program startup - -Before compiling any Simplicity programs, call `init_external_jet_lib` with the path to the compiled `.so` / `.dylib` / `.dll`: - -```rust -unsafe { - simplicityhl::jet::external::init_external_jet_lib("/path/to/libexternal_jet_lib_example.dylib") - .expect("failed to load external jet lib"); -} -``` - -### 4. Pass `ExternalJetHinter` to the compiler - -`ExternalJetHinter` implements `JetHinter` and delegates `parse_jet` / `construct_verify` to the loaded library. Pass it when constructing a `TemplateAst`: - -```rust -use simplicityhl::{jet::external::ExternalJetHinter, TemplateAst}; - -let program = TemplateAst::new(simf_code, Box::new(ExternalJetHinter::new())) - .expect("compilation failed"); -``` - -The compiler will call `parse_jet` whenever it encounters an unknown jet name, forwarding the lookup to your shared library. - -## Building the Example - -```sh -# Build the shared library -cargo build -p external-jet-lib-example - -# Run the consumer binary, pointing it at the compiled dylib -cargo run -p external-jet-lib-example --bin external-lib-consumer -- \ - target/debug/libexternal_jet_lib_example.dylib -``` - -## Security Notes - -Loading a shared library executes arbitrary native code in the current process. Only load libraries from trusted, verified sources. diff --git a/external-jet-lib-example/src/jet.rs b/external-jet-lib-example/src/jet.rs deleted file mode 100644 index 2104aada..00000000 --- a/external-jet-lib-example/src/jet.rs +++ /dev/null @@ -1,325 +0,0 @@ -//! Implementation of a custom Simplicity jet family that is compiled into a shared -//! library and loaded into a SimplicityHL compiler at runtime. -//! -//! A *jet* is a named, primitive operation that a Simplicity program can invoke -//! as a single node of its DAG. Conceptually a jet is a "black box": the type -//! checker and the bit machine only know the jet's *interface* (its types, -//! encoding, cost and commitment), while its actual behaviour is fixed by a -//! reference implementation. This crate sits at the meeting point of three -//! layers: -//! -//! # What this module provides -//! -//! [`HappyJet`] is a demonstrative jet family with a single member, -//! [`HappyJet::Verify`]. Implementing [`Jet`] and [`JetHL`] for it is all that is -//! required to inform both the SimplicityHL compiler how to -//! handle the jet. The surrounding crate then re-exports these methods -//! across a C ABI (see the crate root in `lib.rs`) so that a separately -//! compiled SimplicityHL binary can load them from a `.so` / `.dylib` / `.dll` -//! without being rebuilt. - -use std::io::Write; - -use simplicityhl::{ - jet::{JetHL, SourceJetClassification, TargetJetClassification}, - simplicity::{ - decode::Error, - decode_bits, - jet::{type_name::TypeName, Jet}, - BitIter, BitWriter, Cmr, Cost, Error as SimplicityError, - }, -}; - -/// FFI-safe handle that identifies jet of this library across the dynamic -/// library boundary. -/// -/// Trait objects (`Box`) and Rust enums have no stable ABI, so they -/// cannot be passed safely between two independently compiled binaries. Instead, -/// every jet is represented on the wire by a plain integer -/// [`index`](Self::index), and each side reconstructs the real jet from that -/// number. -/// -/// The host compiler (`simplicityhl`) defines its *own* -/// `jet::external::ExternalJet` with an identical layout (a single `u64`). The -/// two types are deliberately ABI-compatible: values are passed by copy across -/// the boundary, and each side maps the `index` back to its local representation -/// ([`HappyJet`] here). Keeping the layouts in sync is part of the contract -/// between this library and the loader. -pub struct ExternalJet { - pub index: u64, -} - -/// The jet family defined by this library. -/// -/// This every jet the library knows about. To add another jet you would add -/// a variant here, give it a unique [`index`](Self::index), and extend each -/// method of the [`Jet`] and [`JetHL`] implementations below. The single member, -/// [`HappyJet::Verify`], mirrors the standard Simplicity `verify` jet: it consumes -/// one bit and succeeds only if that bit is `1`, which is exactly -/// the primitive that `assert!` lowers to. -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] -pub enum HappyJet { - Verify, -} - -impl HappyJet { - /// Returns the identifier of this jet. - /// - /// The mapping between variants and indices is the ABI contract that lets a - /// jet cross the library boundary as a bare integer (see [`ExternalJet`]). - /// Every variant has an index and a given index always means the same jet, - /// so existing variants must never be renumbered. - pub fn index(&self) -> u64 { - match self { - HappyJet::Verify => 0, - } - } - - /// Reconstructs a jet from the index produced by [`index`](Self::index). - /// - /// This is the inverse of [`index`](Self::index). It returns [`None`] for an - /// unknown index, which the FFI layer treats as a fatal contract violation - /// (the host should only ever hand back indices this library handed out). - pub fn from_index(index: u64) -> Option { - match index { - 0 => Some(HappyJet::Verify), - _ => None, - } - } -} - -/// rust-simplicity prt of the jet. -/// -/// These methods describe the jet to the Simplicity bit machine itself: its -/// identity, its Simplicity-level types, how it is serialised inside an encoded -/// program, its cost, and how its name is parsed. Everything here is independent -/// of the high-level language. -impl Jet for HappyJet { - /// Returns the **Commitment Merkle Root** (CMR) of the jet. - /// - /// The CMR is a 32-byte tagged hash that is the cryptographic identity of - /// a Simplicity node. Programs commit to their CMR (for example inside a - /// Taproot output), so the CMR of every leaf feeds into the identity of - /// the whole program. For a jet the CMR is a fixed constant defined by the - /// specification, not something derived at runtime. - /// - /// **Importance:** all parties must agree on this exact value. If two - /// implementations disagree on a jet's CMR they compute different program - /// identities, and a validator that does not recognise the CMR cannot - /// execute the jet. The bytes below are therefore hard-coded and must match - /// the reference definition. - fn cmr(&self) -> Cmr { - let bytes = match self { - HappyJet::Verify => [ - 0xcd, 0xca, 0x2a, 0x05, 0xe5, 0x2c, 0xef, 0xa5, 0x9d, 0xc7, 0xa5, 0xb0, 0xda, 0xe2, - 0x20, 0x98, 0xfb, 0x89, 0x6e, 0x39, 0x13, 0xbf, 0xdd, 0x44, 0x6b, 0x59, 0x4e, 0x1f, - 0x92, 0x50, 0x78, 0x3e, - ], - }; - Cmr::from_byte_array(bytes) - } - - /// Returns the **source** (input) type of the jet as a [`TypeName`]. - /// - /// Because jets are opaque to Simplicity's type-inference engine, their - /// types must be stated explicitly. A [`TypeName`] is a byte string written - /// in prefix (Polish) notation where `1` = unit, `2` = a single bit, - /// `c`/`s`/`i`/`l`/`h` = 8/16/32/64/256-bit words, and `+`/`*` are sum and - /// product. Here `verify` has source `b"2"`, a single bit — i.e. a `bool`, - /// since the boolean type is the sum `1 + 1`. - fn source_ty(&self) -> TypeName { - let name: &'static [u8] = match self { - HappyJet::Verify => b"2", - }; - - TypeName(name) - } - - /// Returns the **target** (output) type of the jet as a [`TypeName`]. - /// - /// Encoded exactly like [`source_ty`](Self::source_ty). `verify` returns - /// `b"1"`, the unit type: it produces no data and is invoked purely for its - /// side effect of asserting that the input bit is `1`. - fn target_ty(&self) -> TypeName { - let name: &'static [u8] = match self { - HappyJet::Verify => b"1", - }; - - TypeName(name) - } - - /// Serialises the jet into the bit-level encoding of a Simplicity program. - /// - /// # Where this fits in the program encoding - /// - /// A Simplicity program is serialised as a list of nodes. The node encoder - /// (rust-simplicity's `bit_encoding::encode::encode_node`) first writes a - /// short prefix that classifies each node; for a jet that prefix is the two - /// bits `1` (this is a *jet-or-word*, not a combinator) then `1` (it is a - /// *jet*, not a constant word). Only *after* those bits does it call this - /// method, so `encode` is responsible solely for the **jet code**: the bits - /// that say *which* jet of this library it is. A complete `verify` node on - /// the wire is therefore `1` `1` followed by the single bit produced here: - /// - /// ```text - /// 1 1 0 - /// │ │ └── jet code from HappyJet::encode (selects `verify`) - /// │ └──── node is a jet (not a constant word) - /// └────── node is a jet-or-word (not a combinator) - /// ``` - /// - /// # The jet code is a prefix-free tree - /// - /// The jets of a family are the leaves of a binary tree, and a jet's code is - /// the path of `0`/`1` branches from the root down to its leaf. The pair - /// `(n, len)` passed to `write_bits_be` *is* that path: `len` is the depth - /// and the low `len` bits of `n` (most-significant first) are the branch - /// directions. Since no jet's path is a prefix of another's, the decoder can - /// find where the code ends without a separate length field. - /// - /// This library's tree has a **single** leaf, so the path is just the one - /// bit `0` — hence `(n, len) = (0, 1)`. Real jet sets have many leaves and - /// thus longer codes (see below). - /// - /// The code here **must** stay in lock-step with [`decode`](Self::decode), - /// which walks the same tree in reverse. - fn encode(&self, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result { - let (n, len) = match self { - HappyJet::Verify => (0, 1), - }; - - w.write_bits_be(n, len) - } - - /// Reconstructs a jet from the bit encoding produced by - /// [`encode`](Self::encode). - /// - /// Decoding is the mirror of encoding: starting at the root of the jet tree, - /// each incoming bit chooses a branch (`0` = first/left, `1` = second/right) - /// until a leaf is reached. The [`decode_bits!`] macro - /// builds exactly that decision tree. Here the tree has a single real leaf: - /// - /// ```text - /// root ─┬─ 0 ─> HappyJet::Verify - /// └─ 1 ─> (empty, reserved for future jets) - /// ``` - fn decode>(bits: &mut BitIter) -> Result - where - Self: Sized, - { - decode_bits!(bits, { - 0 => {HappyJet::Verify}, - 1 => {} - }) - } - - /// Returns the execution **cost** of the jet, in milliweight units. - /// - /// Simplicity bounds CPU usage with a cost model: the summed cost of every - /// executed node must stay within the program's *budget*, which is funded by - /// the size of the transaction's witness stack. One weight unit is 1000 - /// milliweight, so `from_milliweight(44)` is 0.044 weight units. The value - /// should reflect the real cost of the operation and match the figure, - /// otherwise budgets computed by different parties will disagree. - fn cost(&self) -> Cost { - match self { - HappyJet::Verify => Cost::from_milliweight(44), - } - } - - /// Parses the textual name of a jet into the corresponding variant. - /// - /// This is what resolves the identifier written after `jet::` in SimplicityHL - /// source (and in Simplicity s-expressions). The name `"verify"` maps to - /// [`HappyJet::Verify`. It is the inverse of the [`Display`](std::fmt::Display) - /// implementation. - fn parse(s: &str) -> Result - where - Self: Sized, - { - match s { - "verify" => Ok(HappyJet::Verify), - x => Err(SimplicityError::InvalidJetName(x.to_owned())), - } - } -} - -/// Renders the jet back to its textual name. -/// -/// This is the inverse of [`HappyJet::parse`] and is used in error messages, -/// debug output and any text serialisation of a program. The two must -/// round-trip: `HappyJet::parse(&jet.to_string()) == Ok(jet)`. -impl std::fmt::Display for HappyJet { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - match self { - HappyJet::Verify => write!(f, "verify"), - } - } -} - -/// SimplicityHL (high-level language) view of the jet. -/// -/// Where [`Jet`] describes the jet in raw Simplicity terms (bit-level types), -/// [`JetHL`] describes how it appears in SimplicityHL's richer type system. The -/// compiler uses this to type-check call sites such as `jet::verify(x)` and to -/// bind the jet's arguments and result to high-level types. -impl JetHL for HappyJet { - /// Describes how the Simplicity source type is split into SimplicityHL - /// argument types. - /// - /// For the regular the compiler assumes the input is a balanced product of - /// equal-width integers and derives each argument type from the total bit - /// width. `verify` does not fit that mould — its single bit is a *boolean*, - /// not a 1-bit integer — so we use - /// [`Custom`](SourceJetClassification::Custom) to state the argument list - /// explicitly as `[bool]`. - fn source_jet_classification(&self) -> SourceJetClassification { - match self { - HappyJet::Verify => SourceJetClassification::Custom(vec![simplicityhl::jet::bool()]), - } - } - - /// Describes the SimplicityHL **return** type of the jet. - /// - /// [`Unary`](TargetJetClassification::Unary) asks the compiler to derive a - /// single value from the target bit width. Because `verify`'s target is unit - /// (bit width 0), that derivation yields the unit type `()`; the jet is used - /// for its assertion side-effect rather than a return value. Use - /// [`Custom`](TargetJetClassification::Custom) when the output is not a plain - /// integer. - fn target_jet_classification(&self) -> TargetJetClassification { - match self { - HappyJet::Verify => TargetJetClassification::Unary, - } - } - - /// Reports whether the jet may be referenced *directly* by name in - /// SimplicityHL source. - /// - /// When this returns `true`, the compiler treats `jet::` as - /// non-existent at the call site even though it can still be parsed. The - /// built-in `Core`/`Elements` sets disable their `verify` jet so that users - /// reach it only through `assert!`. This example leaves it **enabled**, so - /// both `jet::verify(x)` and `assert!(x)` work, the latter is lowered through - /// the hinter's `construct_verify`, independently of this flag. - fn is_disabled(&self) -> bool { - false - } - - /// Clones the jet into a fresh `Box`. - /// - /// [`JetHL`] is used as a trait object, and trait objects cannot implement - /// [`Clone`] directly. This method provides the object-safe cloning that - /// `Box: Clone` forwards to. - fn clone_box(&self) -> Box { - Box::new(*self) - } - - /// Returns the jet as a lower-level [`Jet`] trait object (`&dyn Jet`). - /// - /// The compiler needs a `&dyn Jet` to build Simplicity nodes from a - /// `&dyn JetHL`; this method performs that upcast explicitly. - fn as_jet(&self) -> &dyn Jet { - self - } -} diff --git a/external-jet-lib-example/src/lib.rs b/external-jet-lib-example/src/lib.rs deleted file mode 100644 index c2eba00c..00000000 --- a/external-jet-lib-example/src/lib.rs +++ /dev/null @@ -1,173 +0,0 @@ -//! C-ABI surface of the external jet library. -//! -//! This is the boundary that turns [`HappyJet`] into a runtime *plugin*. The -//! crate is built as a `cdylib`, and every `#[no_mangle]` function below is -//! exported as a symbol that the host SimplicityHL compiler resolves by name -//! (via `dlopen` / `LoadLibraryW`) into its `ExternalJetLib` function-pointer -//! table. At runtime the host calls these symbols to ask the library to describe -//! and construct its jets. We use the `#[no_mangle]` attribute to prevent Rust -//! from changing the symbol names. -//! -//! # The bridging pattern -//! -//! The host speaks in terms of the FFI-safe [`ExternalJet`] handle (an integer -//! index), while all real behaviour lives on [`HappyJet`]. Almost every export -//! therefore follows the same three steps: -//! -//! 1. receive an [`ExternalJet`] (or a name, or a `&dyn Jet`), -//! 2. rebuild the real [`HappyJet`] from it with -//! [`HappyJet::from_index`], and -//! 3. delegate to the corresponding [`Jet`] or [`JetHL`] method, returning the -//! result by value. -//! -//! Because the host should only ever pass back indices this library handed out, -//! an unknown index is a fatal ABI violation rather than a recoverable error, so -//! the `from_index(..).expect(..)` calls below intentionally panic. -//! -//! # ABI contract -//! -//! The set of symbol names and their exact signatures must match -//! `ExternalJetLib::load` in the host crate (`src/jet/external.rs`). The loader -//! transmutes each resolved address into a Rust `fn` pointer **without** checking -//! the signature, so any mismatch is undefined behaviour. These functions use the -//! default Rust ABI rather than `extern "C"`, that is sound only because the host -//! and this library are built with the same toolchain and share the exact same -//! `simplicity` / `simplicityhl` types. -//! -//! # Safety -//! -//! Loading and calling this library executes arbitrary native code in the host -//! process. Only load libraries built from trusted, verified sources. - -use std::io::Write; - -use simplicityhl::{ - jet::{JetHL, SourceJetClassification, TargetJetClassification}, - simplicity::{ - jet::{type_name::TypeName, Jet}, - BitWriter, Cmr, Cost, - }, -}; - -use crate::jet::{ExternalJet, HappyJet}; - -/// Jet definitions ([`HappyJet`]) and their [`Jet`]/[`JetHL`] implementations. -pub mod jet; - -/// Exports [`HappyJet::cmr`]: the jet's Commitment Merkle Root (its identity). -#[no_mangle] -pub fn cmr(jet: ExternalJet) -> Cmr { - let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); - - jet.cmr() -} - -/// Exports [`HappyJet::source_ty`]: the jet's Simplicity source (input) type. -#[no_mangle] -pub fn source_ty(jet: ExternalJet) -> TypeName { - let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); - - jet.source_ty() -} - -/// Exports [`HappyJet::target_ty`]: the jet's Simplicity target (output) type. -#[no_mangle] -pub fn target_ty(jet: ExternalJet) -> TypeName { - let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); - - jet.target_ty() -} - -/// Exports [`HappyJet::encode`]: serialises the jet into a program's bit stream. -/// -/// The host passes its own [`BitWriter`] (the bit-level framing the Simplicity -/// encoding expects) wrapping the underlying byte sink, and this export simply -/// delegates to [`HappyJet::encode`]. The signature must match the -/// `ExternalJetLib::encode` field in the host crate exactly — see the -/// module-level note on the ABI contract. -#[no_mangle] -pub fn encode(jet: ExternalJet, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result { - let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); - - jet.encode(w) -} - -/// Exports [`HappyJet::cost`]: the jet's execution cost (in milliweight units). -#[no_mangle] -pub fn cost(jet: ExternalJet) -> Cost { - let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); - - jet.cost() -} - -/// Exports [`HappyJet::parse`]: resolves a jet name to a handle. -/// -/// Unlike most exports this takes a name rather than an [`ExternalJet`]: it is -/// how the host turns the identifier written after `jet::` into a handle. On -/// success the resulting [`HappyJet`] is reduced to its [`ExternalJet`] index for -/// return across the boundary. -#[no_mangle] -pub fn parse(s: &str) -> Result { - HappyJet::parse(s).map(|jet| ExternalJet { index: jet.index() }) -} -/// Exports the [`Display`](std::fmt::Display) name of the jet. -#[no_mangle] -pub fn display(jet: ExternalJet) -> String { - let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); - - jet.to_string() -} - -/// Exports [`JetHL::source_jet_classification`]: how the compiler splits the -/// source type into high-level argument types. -#[no_mangle] -pub fn source_jet_classification(jet: ExternalJet) -> SourceJetClassification { - let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); - - jet.source_jet_classification() -} - -/// Exports [`JetHL::target_jet_classification`]: the high-level return type of -/// the jet. -#[no_mangle] -pub fn target_jet_classification(jet: ExternalJet) -> TargetJetClassification { - let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); - - jet.target_jet_classification() -} - -/// Exports [`JetHL::is_disabled`]: whether the jet may be named directly in -/// SimplicityHL source. -#[no_mangle] -pub fn is_disabled(jet: ExternalJet) -> bool { - let jet = HappyJet::from_index(jet.index).expect("invalid jet index"); - - jet.is_disabled() -} - -/// Constructs the library's `verify` jet handle. -/// -/// The host's `ExternalJetHinter::construct_verify` calls this when lowering -/// `assert!`, so this single export is what makes assertions work with the -/// external jet set. It returns the [`ExternalJet`] index of -/// [`HappyJet::Verify`]. -#[no_mangle] -pub fn verify() -> ExternalJet { - let jet = HappyJet::Verify; - - ExternalJet { index: jet.index() } -} - -/// Recovers the high-level jet from a bare runtime Simplicity jet. -/// -/// Given a type-erased `&dyn Jet`, for example one obtained while inspecting or -/// tracing an already-built program, this attempts to downcast it to -/// [`HappyJet`] and re-box it as a [`JetHL`], re-attaching the high-level -/// behaviour. It returns [`None`] if the jet does not belong to this library. -/// This mirrors the `conjure` method of the built-in jet hinters. -#[no_mangle] -pub fn conjure(jet: &dyn Jet) -> Option> { - jet.as_any() - .downcast_ref::() - .map(|jet| Box::new(*jet) as Box) -} diff --git a/external-jet-lib-example/src/main.rs b/external-jet-lib-example/src/main.rs deleted file mode 100644 index 1ff0eab3..00000000 --- a/external-jet-lib-example/src/main.rs +++ /dev/null @@ -1,52 +0,0 @@ -//! Minimal *consumer* of the external jet library. -//! -//! This binary plays the role of a host SimplicityHL application that wants to -//! compile programs against a jet set supplied by a *separately compiled* shared -//! library. It demonstrates the full runtime lifecycle: load the library, -//! register it globally, then compile a program whose jets are resolved through -//! it. -//! -//! Run it with the path to the compiled library as the first argument: -//! -//! ```sh -//! cargo run -p external-jet-lib-example --bin external-lib-consumer -- \ -//! target/debug/libexternal_jet_lib_example.dylib -//! ``` -//! -//! # Safety -//! -//! [`init_external_jet_lib`](simplicityhl::jet::external::init_external_jet_lib) -//! loads and runs native code from the given path. Only point it at libraries -//! you trust. - -use simplicityhl::{jet::external::ExternalJetHinter, TemplateAst}; - -/// Loads the external jet library named on the command line and compiles a tiny -/// SimplicityHL program against it. -fn main() { - // 1. The path to the compiled `.so` / `.dylib` / `.dll` is taken from argv. - let lib_path = std::env::args().nth(1).expect( - "Please provide the path to the compiled external jet library as the first argument", - ); - - // 2. `dlopen` the library and resolve all of its `#[no_mangle]` exports into - // the process-global `ExternalJetLib` table. This must happen exactly once - // and before any program that uses external jets is compiled. - unsafe { - simplicityhl::jet::external::init_external_jet_lib(&lib_path) - .expect("failed to initialize external jet lib"); - } - - let code = r#"fn main() { - assert!(true); -}"#; - - // 3. Compile the program. `ExternalJetHinter` routes every jet lookup - // (`parse_jet`, `construct_verify`, `conjure`) to the loaded library; here - // `assert!(true)` is lowered via `construct_verify` to the library's - // `verify` jet. - let _ = TemplateAst::new(code, Box::new(ExternalJetHinter::new())) - .expect("failed to compile code with external jets"); - - println!("External jets were successfully used to compile:\n{}", code); -} diff --git a/src/jet/dynlib.rs b/src/jet/dynlib.rs deleted file mode 100644 index 553ca409..00000000 --- a/src/jet/dynlib.rs +++ /dev/null @@ -1,184 +0,0 @@ -//! Minimal cross-platform dynamic library loader. - -use std::ffi::c_void; -use std::fmt; -use std::path::Path; - -/// Error returned by dynamic library operations. -#[derive(Debug)] -pub struct Error(String); - -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&self.0) - } -} - -impl std::error::Error for Error {} - -/// Handle to a dynamically loaded shared library. -/// -/// The library is unloaded when this value is dropped. -pub struct Library { - handle: *mut c_void, -} - -unsafe impl Send for Library {} -unsafe impl Sync for Library {} - -impl Library { - /// Load a shared library from the given filesystem path. - /// - /// # Safety - /// - /// The caller must ensure that the specified path is valid - /// and points to shared library compatible with the current platform. - pub unsafe fn load(path: &Path) -> Result { - imp::load(path).map(|handle| Self { handle }) - } - - /// Look up an exported symbol by name and return its address as a - /// function pointer of type `F`. - /// - /// # Safety - /// - /// The caller is responsible for ensuring that the type `F` matches the - /// actual signature of the symbol in the loaded library. Calling a - /// function through a mismatched signature is undefined behavior. - pub unsafe fn symbol(&self, name: &str) -> Result { - assert_eq!( - std::mem::size_of::(), - std::mem::size_of::<*mut c_void>(), - "symbol type must be pointer-sized (e.g. a function pointer)", - ); - let ptr = imp::symbol(self.handle, name)?; - // SAFETY: caller asserts that `F` matches the symbol signature, and - // `F` is pointer-sized as checked above. - Ok(std::mem::transmute_copy::<*mut c_void, F>(&ptr)) - } -} - -impl Drop for Library { - fn drop(&mut self) { - unsafe { imp::close(self.handle) }; - } -} - -#[cfg(unix)] -mod imp { - use super::Error; - use std::ffi::{c_void, CString}; - use std::path::Path; - - extern "C" { - fn dlopen(filename: *const i8, flag: i32) -> *mut c_void; - fn dlsym(handle: *mut c_void, symbol: *const i8) -> *mut c_void; - fn dlclose(handle: *mut c_void) -> i32; - fn dlerror() -> *const i8; - } - - const RTLD_NOW: i32 = 2; - const RTLD_LOCAL: i32 = 0; - - fn last_error(fallback: &str) -> Error { - // SAFETY: `dlerror` returns either NULL or a pointer to a - // NUL-terminated C string owned by libc; we copy it immediately. - unsafe { - let ptr = dlerror(); - if ptr.is_null() { - return Error(fallback.to_owned()); - } - let mut len = 0; - while *ptr.add(len) != 0 { - len += 1; - } - let bytes = std::slice::from_raw_parts(ptr.cast::(), len); - Error(String::from_utf8_lossy(bytes).into_owned()) - } - } - - pub(super) fn load(path: &Path) -> Result<*mut c_void, Error> { - let path_str = path.to_str().ok_or_else(|| { - Error(format!( - "library path is not valid UTF-8: {}", - path.display() - )) - })?; - let c_path = CString::new(path_str) - .map_err(|_| Error("library path contains an interior NUL byte".to_owned()))?; - // Clear any stale error before the call so `dlerror` reflects this call. - unsafe { dlerror() }; - let handle = unsafe { dlopen(c_path.as_ptr(), RTLD_NOW | RTLD_LOCAL) }; - if handle.is_null() { - return Err(last_error("dlopen failed")); - } - Ok(handle) - } - - pub(super) fn symbol(handle: *mut c_void, name: &str) -> Result<*mut c_void, Error> { - let c_name = CString::new(name) - .map_err(|_| Error(format!("symbol name contains a NUL byte: {name}")))?; - unsafe { dlerror() }; - let ptr = unsafe { dlsym(handle, c_name.as_ptr()) }; - if ptr.is_null() { - // A NULL return is only an error if `dlerror` reports one (a - // symbol may legitimately resolve to NULL). - let err = last_error("symbol not found"); - return Err(Error(format!("failed to load symbol '{name}': {err}"))); - } - Ok(ptr) - } - - pub(super) unsafe fn close(handle: *mut c_void) { - let _ = dlclose(handle); - } -} - -#[cfg(windows)] -mod imp { - use super::Error; - use std::ffi::{c_void, CString}; - use std::path::Path; - - #[link(name = "kernel32")] - extern "system" { - fn LoadLibraryW(filename: *const u16) -> *mut c_void; - fn GetProcAddress(module: *mut c_void, name: *const i8) -> *mut c_void; - fn FreeLibrary(module: *mut c_void) -> i32; - fn GetLastError() -> u32; - } - - fn last_error(prefix: &str) -> Error { - let code = unsafe { GetLastError() }; - Error(format!("{prefix} (Windows error {code})")) - } - - pub(super) fn load(path: &Path) -> Result<*mut c_void, Error> { - // Encode path as a NUL-terminated UTF-16 sequence for `LoadLibraryW`. - use std::os::windows::ffi::OsStrExt; - let mut wide: Vec = path.as_os_str().encode_wide().collect(); - wide.push(0); - let handle = unsafe { LoadLibraryW(wide.as_ptr()) }; - if handle.is_null() { - return Err(last_error(&format!( - "LoadLibraryW failed for {}", - path.display() - ))); - } - Ok(handle) - } - - pub(super) fn symbol(handle: *mut c_void, name: &str) -> Result<*mut c_void, Error> { - let c_name = CString::new(name) - .map_err(|_| Error(format!("symbol name contains a NUL byte: {name}")))?; - let ptr = unsafe { GetProcAddress(handle, c_name.as_ptr()) }; - if ptr.is_null() { - return Err(last_error(&format!("failed to load symbol '{name}'"))); - } - Ok(ptr) - } - - pub(super) unsafe fn close(handle: *mut c_void) { - let _ = FreeLibrary(handle); - } -} diff --git a/src/jet/external.rs b/src/jet/external.rs deleted file mode 100644 index 44bba842..00000000 --- a/src/jet/external.rs +++ /dev/null @@ -1,257 +0,0 @@ -use std::sync::OnceLock; -use std::{io::Write, path::Path}; - -use simplicity::{ - jet::{type_name::TypeName, Jet}, - BitIter, BitWriter, Cmr, Cost, -}; - -use crate::ast::JetHinter; -use crate::jet::dynlib::Library; -use crate::jet::{JetHL, SourceJetClassification, TargetJetClassification}; - -static EXTERNAL_JET_LIB: OnceLock = OnceLock::new(); - -/// Load the external jet library from the specified shared object file path -/// -/// # Safety -/// -/// The caller must ensure that the loaded library exports each of the -/// symbols listed below with signatures matching the corresponding -/// fields of [`ExternalJetLib`]. Calling a function through a -/// mismatched signature is undefined behavior. -pub unsafe fn init_external_jet_lib(path: &str) -> Result<(), Box> { - let library = unsafe { Library::load(Path::new(path))? }; - let api = unsafe { ExternalJetLib::load(library)? }; - - if EXTERNAL_JET_LIB.set(api).is_err() { - return Err("Failed to set external jet lib, it may have already been initialized".into()); - } - - Ok(()) -} - -fn external_jet_lib() -> &'static ExternalJetLib { - EXTERNAL_JET_LIB - .get() - .expect("External jet lib is not initialized. Please call init_external_jet_lib first.") -} - -/// Symbol table loaded from an external jet shared library. -/// -/// Each field is a function pointer resolved from a `#[no_mangle]` export of -/// the same name in the library. -pub struct ExternalJetLib { - cmr: fn(jet: ExternalJet) -> Cmr, - source_ty: fn(jet: ExternalJet) -> TypeName, - target_ty: fn(jet: ExternalJet) -> TypeName, - encode: fn(jet: ExternalJet, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result, - cost: fn(jet: ExternalJet) -> Cost, - parse: fn(s: &str) -> Result, - display: fn(jet: ExternalJet) -> String, - - source_jet_classification: fn(jet: ExternalJet) -> SourceJetClassification, - target_jet_classification: fn(jet: ExternalJet) -> TargetJetClassification, - is_disabled: fn(jet: ExternalJet) -> bool, - - verify: fn() -> ExternalJet, - conjure: fn(jet: &dyn Jet) -> Option>, - - // Keep the library loaded, symbols above are only valid while it lives. - _library: Library, -} - -impl ExternalJetLib { - /// Resolve all required symbols from `library`. - /// - /// # Safety - /// - /// The caller must ensure that the loaded library exports each of the - /// symbols listed below with signatures matching the corresponding - /// fields of [`ExternalJetLib`]. Calling a function through a - /// mismatched signature is undefined behavior. - unsafe fn load(library: Library) -> Result> { - let cmr = library.symbol("cmr")?; - let source_ty = library.symbol("source_ty")?; - let target_ty = library.symbol("target_ty")?; - let encode = library.symbol("encode")?; - let cost = library.symbol("cost")?; - let parse = library.symbol("parse")?; - let display = library.symbol("display")?; - let source_jet_classification = library.symbol("source_jet_classification")?; - let target_jet_classification = library.symbol("target_jet_classification")?; - let is_disabled = library.symbol("is_disabled")?; - let verify = library.symbol("verify")?; - let conjure = library.symbol("conjure")?; - - Ok(Self { - cmr, - source_ty, - target_ty, - encode, - cost, - parse, - display, - source_jet_classification, - target_jet_classification, - is_disabled, - verify, - conjure, - _library: library, - }) - } -} - -#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] -pub struct ExternalJet { - pub index: u64, -} - -impl ExternalJet { - pub fn new(index: u64) -> Self { - Self { index } - } -} - -impl Jet for ExternalJet { - fn cmr(&self) -> Cmr { - let container = external_jet_lib(); - (container.cmr)(*self) - } - - fn source_ty(&self) -> TypeName { - let container = external_jet_lib(); - (container.source_ty)(*self) - } - - fn target_ty(&self) -> TypeName { - let container = external_jet_lib(); - (container.target_ty)(*self) - } - - fn encode(&self, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result { - let container = external_jet_lib(); - (container.encode)(*self, w) - } - - fn decode>( - _bits: &mut BitIter, - ) -> Result - where - Self: Sized, - { - unimplemented!("Decoding is not implemented for ExternalJet for now") - } - - fn cost(&self) -> Cost { - let container = external_jet_lib(); - (container.cost)(*self) - } - - fn parse(s: &str) -> Result - where - Self: Sized, - { - let container = external_jet_lib(); - (container.parse)(s) - } -} - -impl std::fmt::Display for ExternalJet { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let container = external_jet_lib(); - let display_str = (container.display)(*self); - write!(f, "{}", display_str) - } -} - -impl JetHL for ExternalJet { - fn source_jet_classification(&self) -> SourceJetClassification { - let container = external_jet_lib(); - (container.source_jet_classification)(*self) - } - - fn target_jet_classification(&self) -> TargetJetClassification { - let container = external_jet_lib(); - (container.target_jet_classification)(*self) - } - - fn is_disabled(&self) -> bool { - let container = external_jet_lib(); - (container.is_disabled)(*self) - } - - fn clone_box(&self) -> Box { - Box::new(*self) - } - - fn as_jet(&self) -> &dyn Jet { - self - } -} - -#[derive(Clone, Debug, Default)] -pub struct ExternalJetHinter; - -impl ExternalJetHinter { - pub fn new() -> Self { - Self - } -} - -impl JetHinter for ExternalJetHinter { - fn parse_jet(&self, name: &str) -> Option> { - let container = external_jet_lib(); - match (container.parse)(name) { - Ok(jet) => Some(Box::new(jet)), - Err(_) => None, - } - } - - fn construct_verify(&self) -> Box { - let container = external_jet_lib(); - let jet = (container.verify)(); - Box::new(jet) - } - - fn clone_box(&self) -> Box { - Box::new(ExternalJetHinter) - } - - fn construct_sha_256_ctx_8_init(&self) -> Option> { - self.parse_jet("sha_256_ctx_8_init") - } - - fn construct_sha_256_ctx_8_add_1(&self) -> Option> { - self.parse_jet("sha_256_ctx_8_add_1") - } - - fn construct_sha_256_ctx_8_add_2(&self) -> Option> { - self.parse_jet("sha_256_ctx_8_add_2") - } - - fn construct_sha_256_ctx_8_add_4(&self) -> Option> { - self.parse_jet("sha_256_ctx_8_add_4") - } - - fn construct_sha_256_ctx_8_add_8(&self) -> Option> { - self.parse_jet("sha_256_ctx_8_add_8") - } - - fn construct_sha_256_ctx_8_add_16(&self) -> Option> { - self.parse_jet("sha_256_ctx_8_add_16") - } - - fn construct_sha_256_ctx_8_add_32(&self) -> Option> { - self.parse_jet("sha_256_ctx_8_add_32") - } - - fn construct_sha_256_ctx_8_finalize(&self) -> Option> { - self.parse_jet("sha_256_ctx_8_finalize") - } - - fn conjure(&self, jet: &dyn Jet) -> Option> { - let container = external_jet_lib(); - (container.conjure)(jet) - } -} diff --git a/src/jet/mod.rs b/src/jet/mod.rs index 9fb3e200..aeb30bd7 100644 --- a/src/jet/mod.rs +++ b/src/jet/mod.rs @@ -1,9 +1,5 @@ pub mod core; -#[cfg(feature = "external-jets")] -mod dynlib; pub mod elements; -#[cfg(feature = "external-jets")] -pub mod external; use crate::num::NonZeroPow2Usize; use crate::num::Pow2Usize;