Skip to content

Commit d5d5cf8

Browse files
committed
external jets loading
1 parent ffa948e commit d5d5cf8

11 files changed

Lines changed: 762 additions & 1 deletion

File tree

.github/workflows/rust.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,22 @@ jobs:
106106
echo "fuzzing $target"
107107
cargo fuzz run --fuzz-dir fuzz "$target" -- -runs=1
108108
done
109+
110+
ExternalJetLib:
111+
name: External Jet Library
112+
runs-on: ubuntu-latest
113+
steps:
114+
- name: "Checkout repo"
115+
uses: actions/checkout@v4
116+
117+
- name: "Setup cargo-rbmt"
118+
uses: ./.github/actions/setup-rbmt
119+
120+
- name: "Build external jet dylib"
121+
run: cargo rbmt --lock-file existing run --toolchain stable -- build --manifest-path external-jet-lib-example/Cargo.toml --lib
122+
123+
- name: "Build external-lib-consumer binary"
124+
run: cargo rbmt --lock-file existing run --toolchain stable -- build --manifest-path external-jet-lib-example/Cargo.toml --bin external-lib-consumer
125+
126+
- name: "Run external-lib-consumer with compiled dylib"
127+
run: ./target/debug/external-lib-consumer ./target/debug/libexternal_jet_lib_example.so

Cargo.lock

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ required-features = ["docs", "serde"]
3131
default = [ "serde" ]
3232
serde = ["dep:serde", "dep:serde_json"]
3333
docs = []
34+
external-jets = []
3435

3536
[dependencies]
3637
base64 = "0.21.2"
@@ -48,7 +49,7 @@ chumsky = "0.11.2"
4849
getrandom = { version = "0.2", features = ["js"] }
4950

5051
[workspace]
51-
members = ["codegen", "fuzz"]
52+
members = ["codegen", "fuzz", "external-jet-lib-example"]
5253
exclude = ["bitcoind-tests"]
5354

5455
[workspace.metadata.rbmt.toolchains]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "external-jet-lib-example"
3+
version = "0.1.0"
4+
edition = "2021"
5+
rust-version = "1.79.0"
6+
7+
[[bin]]
8+
name = "external-lib-consumer"
9+
path = "src/main.rs"
10+
11+
[lib]
12+
crate-type = ["cdylib"]
13+
14+
[dependencies]
15+
simplicityhl = { path = "..", features = ["external-jets"] }

external-jet-lib-example/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# External Jet Loading
2+
3+
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.
4+
5+
## How It Works
6+
7+
### 1. Enable the feature flag
8+
9+
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:
10+
11+
```toml
12+
[dependencies]
13+
simplicityhl = { path = "..", features = ["external-jets"] }
14+
```
15+
16+
### 2. Build a `cdylib` that exports the required symbols
17+
18+
See [src/lib.rs](src/lib.rs) and [src/jet.rs](src/jet.rs) for a minimal implementation with a single `verify` jet.
19+
20+
### 3. Initialize the library at program startup
21+
22+
Before compiling any Simplicity programs, call `init_external_jet_lib` with the path to the compiled `.so` / `.dylib` / `.dll`:
23+
24+
```rust
25+
unsafe {
26+
simplicityhl::jet::external::init_external_jet_lib("/path/to/libexternal_jet_lib_example.dylib")
27+
.expect("failed to load external jet lib");
28+
}
29+
```
30+
31+
### 4. Pass `ExternalJetHinter` to the compiler
32+
33+
`ExternalJetHinter` implements `JetHinter` and delegates `parse_jet` / `construct_verify` to the loaded library. Pass it when constructing a `TemplateProgram`:
34+
35+
```rust
36+
use simplicityhl::{jet::external::ExternalJetHinter, TemplateProgram};
37+
38+
let program = TemplateProgram::new(simf_code, Box::new(ExternalJetHinter::new()))
39+
.expect("compilation failed");
40+
```
41+
42+
The compiler will call `parse_jet` whenever it encounters an unknown jet name, forwarding the lookup to your shared library.
43+
44+
## Building the Example
45+
46+
```sh
47+
# Build the shared library
48+
cargo build -p external-jet-lib-example
49+
50+
# Run the consumer binary, pointing it at the compiled dylib
51+
cargo run -p external-jet-lib-example --bin external-lib-consumer -- \
52+
target/debug/libexternal_jet_lib_example.dylib
53+
```
54+
55+
## Security Notes
56+
57+
Loading a shared library executes arbitrary native code in the current process. Only load libraries from trusted, verified sources.
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
use std::io::Write;
2+
3+
use simplicityhl::{
4+
jet::{JetHL, SourceJetClassification, TargetJetClassification},
5+
simplicity::{
6+
decode::Error,
7+
decode_bits,
8+
jet::{type_name::TypeName, Jet},
9+
BitIter, BitWriter, Cmr, Cost, Error as SimplicityError,
10+
},
11+
};
12+
13+
pub struct ExternalJet {
14+
pub index: u64,
15+
}
16+
17+
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
18+
pub enum HappyJet {
19+
Verify,
20+
}
21+
22+
impl HappyJet {
23+
pub fn index(&self) -> u64 {
24+
match self {
25+
HappyJet::Verify => 0,
26+
}
27+
}
28+
29+
pub fn from_index(index: u64) -> Option<Self> {
30+
match index {
31+
0 => Some(HappyJet::Verify),
32+
_ => None,
33+
}
34+
}
35+
}
36+
37+
impl Jet for HappyJet {
38+
fn cmr(&self) -> Cmr {
39+
let bytes = match self {
40+
HappyJet::Verify => [
41+
0xcd, 0xca, 0x2a, 0x05, 0xe5, 0x2c, 0xef, 0xa5, 0x9d, 0xc7, 0xa5, 0xb0, 0xda, 0xe2,
42+
0x20, 0x98, 0xfb, 0x89, 0x6e, 0x39, 0x13, 0xbf, 0xdd, 0x44, 0x6b, 0x59, 0x4e, 0x1f,
43+
0x92, 0x50, 0x78, 0x3e,
44+
],
45+
};
46+
Cmr::from_byte_array(bytes)
47+
}
48+
49+
fn source_ty(&self) -> TypeName {
50+
let name: &'static [u8] = match self {
51+
HappyJet::Verify => b"2",
52+
};
53+
54+
TypeName(name)
55+
}
56+
57+
fn target_ty(&self) -> TypeName {
58+
let name: &'static [u8] = match self {
59+
HappyJet::Verify => b"1",
60+
};
61+
62+
TypeName(name)
63+
}
64+
65+
fn encode(&self, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result<usize> {
66+
let (n, len) = match self {
67+
HappyJet::Verify => (0, 1),
68+
};
69+
70+
w.write_bits_be(n, len)
71+
}
72+
73+
fn decode<I: Iterator<Item = u8>>(bits: &mut BitIter<I>) -> Result<Self, Error>
74+
where
75+
Self: Sized,
76+
{
77+
decode_bits!(bits, {
78+
0 => {HappyJet::Verify},
79+
1 => {}
80+
})
81+
}
82+
83+
fn cost(&self) -> Cost {
84+
match self {
85+
HappyJet::Verify => Cost::from_milliweight(44),
86+
}
87+
}
88+
89+
fn parse(s: &str) -> Result<Self, SimplicityError>
90+
where
91+
Self: Sized,
92+
{
93+
match s {
94+
"verify" => Ok(HappyJet::Verify),
95+
x => Err(SimplicityError::InvalidJetName(x.to_owned())),
96+
}
97+
}
98+
}
99+
100+
impl std::fmt::Display for HappyJet {
101+
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
102+
match self {
103+
HappyJet::Verify => write!(f, "verify"),
104+
}
105+
}
106+
}
107+
108+
impl JetHL for HappyJet {
109+
fn source_jet_classification(&self) -> SourceJetClassification {
110+
match self {
111+
HappyJet::Verify => SourceJetClassification::Custom(vec![simplicityhl::jet::bool()]),
112+
}
113+
}
114+
115+
fn target_jet_classification(&self) -> TargetJetClassification {
116+
match self {
117+
HappyJet::Verify => TargetJetClassification::Unary,
118+
}
119+
}
120+
121+
fn is_disabled(&self) -> bool {
122+
false
123+
}
124+
125+
fn clone_box(&self) -> Box<dyn JetHL> {
126+
Box::new(*self)
127+
}
128+
129+
fn as_jet(&self) -> &dyn Jet {
130+
self
131+
}
132+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
use std::io::Write;
2+
3+
use simplicityhl::{
4+
jet::{JetHL, SourceJetClassification, TargetJetClassification},
5+
simplicity::{
6+
jet::{type_name::TypeName, Jet},
7+
BitWriter, Cmr, Cost,
8+
},
9+
};
10+
11+
use crate::jet::{ExternalJet, HappyJet};
12+
13+
mod jet;
14+
15+
#[no_mangle]
16+
pub fn cmr(jet: ExternalJet) -> Cmr {
17+
let jet = HappyJet::from_index(jet.index).expect("invalid jet index");
18+
19+
jet.cmr()
20+
}
21+
22+
#[no_mangle]
23+
pub fn source_ty(jet: ExternalJet) -> TypeName {
24+
let jet = HappyJet::from_index(jet.index).expect("invalid jet index");
25+
26+
jet.source_ty()
27+
}
28+
29+
#[no_mangle]
30+
pub fn target_ty(jet: ExternalJet) -> TypeName {
31+
let jet = HappyJet::from_index(jet.index).expect("invalid jet index");
32+
33+
jet.target_ty()
34+
}
35+
36+
#[no_mangle]
37+
pub fn encode(jet: ExternalJet, w: &mut dyn Write) -> std::io::Result<usize> {
38+
let jet = HappyJet::from_index(jet.index).expect("invalid jet index");
39+
40+
let mut bit_writer = BitWriter::new(w);
41+
42+
jet.encode(&mut bit_writer)
43+
}
44+
45+
#[no_mangle]
46+
pub fn cost(jet: ExternalJet) -> Cost {
47+
let jet = HappyJet::from_index(jet.index).expect("invalid jet index");
48+
49+
jet.cost()
50+
}
51+
52+
#[no_mangle]
53+
pub fn parse(s: &str) -> Result<ExternalJet, simplicityhl::simplicity::Error> {
54+
HappyJet::parse(s).map(|jet| ExternalJet { index: jet.index() })
55+
}
56+
#[no_mangle]
57+
pub fn display(jet: ExternalJet) -> String {
58+
let jet = HappyJet::from_index(jet.index).expect("invalid jet index");
59+
60+
jet.to_string()
61+
}
62+
63+
#[no_mangle]
64+
pub fn source_jet_classification(jet: ExternalJet) -> SourceJetClassification {
65+
let jet = HappyJet::from_index(jet.index).expect("invalid jet index");
66+
67+
jet.source_jet_classification()
68+
}
69+
70+
#[no_mangle]
71+
pub fn target_jet_classification(jet: ExternalJet) -> TargetJetClassification {
72+
let jet = HappyJet::from_index(jet.index).expect("invalid jet index");
73+
74+
jet.target_jet_classification()
75+
}
76+
77+
#[no_mangle]
78+
pub fn is_disabled(jet: ExternalJet) -> bool {
79+
let jet = HappyJet::from_index(jet.index).expect("invalid jet index");
80+
81+
jet.is_disabled()
82+
}
83+
84+
#[no_mangle]
85+
pub fn verify() -> ExternalJet {
86+
let jet = HappyJet::Verify;
87+
88+
ExternalJet { index: jet.index() }
89+
}
90+
91+
#[no_mangle]
92+
pub fn conjure(jet: &dyn Jet) -> Option<Box<dyn JetHL>> {
93+
jet.as_any()
94+
.downcast_ref::<HappyJet>()
95+
.map(|jet| Box::new(*jet) as Box<dyn JetHL>)
96+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
use simplicityhl::{jet::external::ExternalJetHinter, TemplateProgram};
2+
3+
fn main() {
4+
let lib_path = std::env::args().nth(1).expect(
5+
"Please provide the path to the compiled external jet library as the first argument",
6+
);
7+
8+
unsafe {
9+
simplicityhl::jet::external::init_external_jet_lib(&lib_path)
10+
.expect("failed to initialize external jet lib");
11+
}
12+
13+
let code = r#"fn main() {
14+
assert!(true);
15+
}"#;
16+
17+
let _ = TemplateProgram::new(code, Box::new(ExternalJetHinter::new()))
18+
.expect("failed to compile code with external jets");
19+
20+
println!("External jets were successfully used to compile:\n{}", code);
21+
}

0 commit comments

Comments
 (0)