├── .github └── workflows │ ├── build.yml │ ├── clippy.yml │ └── docsrs.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── build.rs ├── derive ├── Cargo.toml └── src │ ├── common │ ├── attr_parsing.rs │ ├── error.rs │ ├── field_processing.rs │ ├── mod.rs │ ├── parsing.rs │ ├── polytag_utils.rs │ └── validation.rs │ ├── export │ ├── codegen.rs │ ├── core.rs │ ├── mod.rs │ ├── parsing.rs │ └── validation.rs │ ├── from_ocaml │ ├── codegen.rs │ └── mod.rs │ ├── lib.rs │ ├── ocaml_describer │ ├── codegen.rs │ └── mod.rs │ ├── tests │ ├── export_tests.rs │ ├── from_ocaml_tests.rs │ ├── ocaml_describer_tests.rs │ ├── to_ocaml_tests.rs │ └── to_ocaml_type_safety_tests.rs │ └── to_ocaml │ ├── codegen.rs │ └── mod.rs ├── docs ├── README.md ├── examples │ ├── README.md │ ├── noalloc_primitives │ │ ├── README.md │ │ ├── dune │ │ ├── noalloc_primitives.ml │ │ └── rust │ │ │ ├── Cargo.toml │ │ │ ├── dune │ │ │ ├── dune-Cargo.toml │ │ │ └── src │ │ │ └── lib.rs │ ├── polymorphic_variants │ │ ├── dune │ │ ├── polymorphic_variants_example.ml │ │ └── rust │ │ │ ├── Cargo.toml │ │ │ ├── dune │ │ │ ├── dune-Cargo.toml │ │ │ └── src │ │ │ └── lib.rs │ ├── records │ │ ├── dune │ │ ├── records_example.ml │ │ └── rust │ │ │ ├── Cargo.toml │ │ │ ├── dune │ │ │ ├── dune-Cargo.toml │ │ │ └── src │ │ │ └── lib.rs │ ├── tuples │ │ ├── dune │ │ ├── rust │ │ │ ├── Cargo.toml │ │ │ ├── dune │ │ │ ├── dune-Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ └── tuples_example.ml │ └── variants │ │ ├── dune │ │ ├── rust │ │ ├── Cargo.toml │ │ ├── dune │ │ ├── dune-Cargo.toml │ │ └── src │ │ │ └── lib.rs │ │ └── variants_example.ml ├── part1-initial-usage-a-brief-overview.md ├── part2-fundamental-concepts.md ├── part3-exporting-rust-functions-to-ocaml.md ├── part4-invoking-ocaml-functions-from-rust.md ├── part5-managing-the-ocaml-runtime-for-rust-driven-programs.md ├── part6-advanced-topics.md └── part7-build-and-link-instructions.md ├── dune-builder ├── Cargo.toml ├── README.md └── src │ └── lib.rs ├── dune-project ├── inspect ├── Cargo.toml ├── examples │ ├── basic_usage.rs │ └── inspect_runtime_example │ │ ├── Cargo.toml │ │ ├── build.rs │ │ ├── ocaml │ │ ├── dune │ │ └── test_types.ml │ │ └── src │ │ └── main.rs ├── src │ ├── inspector.rs │ ├── lib.rs │ └── value_repr.rs └── tests │ └── integration_tests.rs ├── src ├── boxroot.rs ├── closure.rs ├── compile_fail_tests.rs ├── compile_ok_tests.rs ├── conv.rs ├── conv │ ├── from_ocaml.rs │ ├── mapping.rs │ └── to_ocaml.rs ├── describe.rs ├── error.rs ├── lib.rs ├── macros.rs ├── memory.rs ├── mlvalues.rs ├── mlvalues │ ├── bigarray.rs │ └── tag.rs ├── runtime.rs ├── user_guides.rs └── value.rs └── testing ├── ocaml-caller ├── .ocamlformat ├── dune ├── ocaml_rust_caller.ml └── rust │ ├── Cargo.toml │ ├── dune │ ├── dune-Cargo.toml │ └── src │ └── lib.rs └── rust-caller ├── Cargo.toml ├── build.rs ├── ocaml ├── callable.ml └── dune └── src └── lib.rs /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [macos-latest, ubuntu-latest] 15 | ocaml-version: ["5.3.0"] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: OCaml/Opam cache 19 | id: ocaml-interop-opam-cache 20 | uses: actions/cache@v3 21 | with: 22 | path: "~/.opam" 23 | key: ${{ matrix.os }}-${{ matrix.ocaml-version }} 24 | - name: Setup OCaml ${{ matrix.ocaml-version }} 25 | uses: ocaml/setup-ocaml@v3 26 | with: 27 | ocaml-compiler: ${{ matrix.ocaml-version }} 28 | - name: Set Opam env 29 | run: opam env | tr '\n' ' ' >> $GITHUB_ENV 30 | - name: Add Opam switch to PATH 31 | run: opam var bin >> $GITHUB_PATH 32 | - name: Setup Rust 33 | uses: dtolnay/rust-toolchain@v1 34 | with: 35 | toolchain: stable 36 | - name: Setttings for cargo in OSX 37 | if: runner.os == 'macOS' 38 | run: | 39 | echo '[build]' >> ~/.cargo/config 40 | echo 'rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup"]' >> ~/.cargo/config 41 | - name: build 42 | uses: actions-rs/cargo@v1 43 | with: 44 | command: build 45 | - name: test 46 | uses: actions-rs/cargo@v1 47 | with: 48 | command: test 49 | - name: Test derive crate 50 | uses: actions-rs/cargo@v1 51 | with: 52 | command: test 53 | args: -p ocaml-interop-derive 54 | - run: opam install dune alcotest base 55 | - name: Rust caller test 56 | uses: actions-rs/cargo@v1 57 | with: 58 | command: test 59 | args: -p rust-caller 60 | - name: Build OCaml caller 61 | run: cd testing/ocaml-caller; opam exec -- dune build -j 1 62 | - name: OCaml caller test 63 | run: cd testing/ocaml-caller; opam exec -- dune test -f 64 | - name: Test Tuples Example 65 | run: cd docs/examples/tuples; opam exec -- dune test -f 66 | - name: Test Records Example 67 | run: cd docs/examples/records; opam exec -- dune test -f 68 | - name: Test Variants Example 69 | run: cd docs/examples/variants; opam exec -- dune test -f 70 | - name: Test Polymorphic Variants Example 71 | run: cd docs/examples/polymorphic_variants; opam exec -- dune test -f 72 | - name: Test Noalloc Primitives Example 73 | run: cd docs/examples/noalloc_primitives; opam exec -- dune test -f 74 | -------------------------------------------------------------------------------- /.github/workflows/clippy.yml: -------------------------------------------------------------------------------- 1 | name: Clippy 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | clippy: 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: [ubuntu-latest] 15 | ocaml-version: ["5.3.0"] 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: OCaml/Opam cache 19 | id: ocaml-interop-opam-cache 20 | uses: actions/cache@v3 21 | with: 22 | path: "~/.opam" 23 | key: ${{ matrix.os }}-${{ matrix.ocaml-version }} 24 | - name: Setup OCaml ${{ matrix.ocaml-version }} 25 | uses: ocaml/setup-ocaml@v3 26 | with: 27 | ocaml-compiler: ${{ matrix.ocaml-version }} 28 | - name: Set Opam env 29 | run: opam env | tr '\n' ' ' >> $GITHUB_ENV 30 | - name: Add Opam switch to PATH 31 | run: opam var bin >> $GITHUB_PATH 32 | - name: Setup Rust 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: nightly 36 | components: clippy 37 | override: true 38 | - name: lint 39 | uses: actions-rs/clippy-check@v1 40 | with: 41 | token: ${{ secrets.GITHUB_TOKEN }} 42 | args: --all-features --features=without-ocamlopt -- -D warnings 43 | -------------------------------------------------------------------------------- /.github/workflows/docsrs.yml: -------------------------------------------------------------------------------- 1 | name: docs.rs compatibility 2 | 3 | on: 4 | push: 5 | branches: [ main, master ] 6 | pull_request: 7 | 8 | jobs: 9 | docsrs: 10 | runs-on: ubuntu-latest 11 | 12 | container: 13 | image: ghcr.io/rust-lang/crates-build-env/linux:latest 14 | 15 | steps: 16 | - name: Checkout source 17 | uses: actions/checkout@v4 18 | 19 | - name: Set up Rust nightly 20 | uses: actions-rs/toolchain@v1 21 | with: 22 | toolchain: nightly 23 | override: true 24 | 25 | - name: Build docs 26 | env: 27 | DOCS_RS: "1" 28 | run: | 29 | cargo +nightly rustdoc --lib \ 30 | -Zrustdoc-map \ 31 | --no-default-features \ 32 | --features "without-ocamlopt" 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.cm[ix] 4 | *.o 5 | .merlin 6 | _build 7 | .vscode 8 | tmp 9 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ocaml-interop" 3 | version = "0.12.0" # remember to update html_root_url 4 | authors = ["Bruno Deferrari "] 5 | license = "MIT" 6 | description = "Utilities for Rust and OCaml interoperability" 7 | homepage = "https://github.com/tizoc/ocaml-interop" 8 | repository = "https://github.com/tizoc/ocaml-interop" 9 | keywords = ["ocaml", "rust", "ffi", "interop"] 10 | edition = "2021" 11 | exclude = [ 12 | ".github/*", 13 | ] 14 | 15 | [package.metadata.docs.rs] 16 | no-default-features = true 17 | features = [ "without-ocamlopt" ] 18 | 19 | [dependencies] 20 | ocaml-sys = { version = "0.26", features = ["ocaml5"] } 21 | ocaml-boxroot-sys = { version = "0.4.0", default-features = false } 22 | static_assertions = "1.1.0" 23 | ocaml-interop-derive = { path = "derive", version = "0.12.0" } 24 | 25 | [features] 26 | default = ["boxroot"] 27 | boxroot = ["ocaml-boxroot-sys/bundle-boxroot"] 28 | without-ocamlopt = ["ocaml-sys/without-ocamlopt"] 29 | no-caml-startup = [] 30 | link = ["ocaml-sys/link", "boxroot"] 31 | 32 | [workspace] 33 | members = [ 34 | ".", 35 | "derive", 36 | "dune-builder", 37 | "inspect", 38 | "inspect/examples/inspect_runtime_example", 39 | "testing/rust-caller", 40 | "testing/ocaml-caller/rust", 41 | "docs/examples/tuples/rust", 42 | "docs/examples/records/rust", 43 | "docs/examples/variants/rust", 44 | "docs/examples/polymorphic_variants/rust", 45 | "docs/examples/noalloc_primitives/rust" 46 | ] 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Viable Systems and TezEdge Contributors 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test test-examples test-all 2 | 3 | test: 4 | @echo "Running main crate tests..." 5 | cargo test 6 | @echo "Running derive crate tests..." 7 | cargo test -p ocaml-interop-derive 8 | @echo "Running OCaml interop inspect tests..." 9 | cargo test -p ocaml-interop-inspect 10 | 11 | test-examples: 12 | @echo "Running rust-caller tests..." 13 | cargo test -p rust-caller 14 | @echo "Running ocaml-caller tests..." 15 | cd testing/ocaml-caller; opam exec -- dune test -f 16 | @echo "--- Running Documentation Examples ---" 17 | @echo "Running Tuples example (docs/examples/tuples)..." 18 | cd docs/examples/tuples; opam exec -- dune test -f 19 | @echo "Running Records example (docs/examples/records)..." 20 | cd docs/examples/records; opam exec -- dune test -f 21 | @echo "Running Variants example (docs/examples/variants)..." 22 | cd docs/examples/variants; opam exec -- dune test -f 23 | @echo "Running Polymorphic Variants example (docs/examples/polymorphic_variants)..." 24 | cd docs/examples/polymorphic_variants; opam exec -- dune test -f 25 | @echo "Running Noalloc Primitives example (docs/examples/noalloc_primitives)..." 26 | cd docs/examples/noalloc_primitives; opam exec -- dune test -f 27 | @echo "--- Finished Documentation Examples ---" 28 | @echo "Running OCaml interop inspect examples..." 29 | cargo test -p ocaml-interop-inspect --example basic_usage 30 | cd inspect/examples/inspect_runtime_example; cargo run 31 | 32 | test-all: test test-examples 33 | @echo "All tests (crate and examples) completed." 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ocaml-interop 2 | 3 | ![build](https://github.com/tizoc/ocaml-interop/workflows/build/badge.svg) 4 | [![crate](https://img.shields.io/crates/v/ocaml-interop.svg)](https://crates.io/crates/ocaml-interop) 5 | [![documentation](https://docs.rs/ocaml-interop/badge.svg)](https://docs.rs/ocaml-interop) 6 | [![license](https://img.shields.io/crates/l/ocaml-interop.svg)](https://github.com/tizoc/ocaml-interop/blob/master/LICENSE) 7 | 8 | _Zinc-iron alloy coating is used in parts that need very good corrosion protection._ 9 | 10 | **API IS CONSIDERED UNSTABLE AT THE MOMENT AND IS LIKELY TO CHANGE IN THE FUTURE** 11 | 12 | **IMPORTANT: Starting with version `0.11.0` only OCaml 5.x is supported** 13 | 14 | [ocaml-interop](https://github.com/tizoc/ocaml-interop) is an OCaml<->Rust FFI with an emphasis on safety inspired by [caml-oxide](https://github.com/stedolan/caml-oxide), [ocaml-rs](https://github.com/zshipko/ocaml-rs) and [CAMLroot](https://arxiv.org/abs/1812.04905). 15 | 16 | Read the API reference and documentation [here](https://docs.rs/ocaml-interop/). 17 | 18 | Report issues on [Github](https://github.com/tizoc/ocaml-interop/issues). 19 | 20 | ## A quick taste 21 | 22 | ### Convert between plain OCaml and Rust values 23 | 24 | ```rust 25 | let rust_string = ocaml_string.to_rust(); 26 | // `cr` = OCaml runtime handle 27 | let new_ocaml_string = rust_string.to_ocaml(cr); 28 | ``` 29 | 30 | ### Convert between Rust and OCaml structs/records 31 | 32 | ```ocaml 33 | (* OCaml *) 34 | type my_record = { 35 | string_field: string; 36 | tuple_field: (string * int64); 37 | } 38 | ``` 39 | 40 | ```rust 41 | // Rust 42 | #[derive(ToOCaml, FromOCaml)] 43 | struct MyStruct { 44 | string_field: String, 45 | tuple_field: (String, i64), 46 | } 47 | 48 | // ... 49 | 50 | let rust_struct = ocaml_record.to_rust(); 51 | let new_ocaml_record = rust_struct.to_ocaml(cr); 52 | ``` 53 | 54 | ### Convert between OCaml and Rust variants/enums 55 | 56 | ```ocaml 57 | (* OCaml *) 58 | type my_variant = 59 | | EmptyTag 60 | | TagWithInt of int 61 | ``` 62 | 63 | ```rust 64 | // Rust 65 | #[derive(ToOCaml, FromOCaml)] 66 | enum MyEnum { 67 | EmptyTag, 68 | TagWithInt(#[ocaml(as_ = "OCamlInt")] i64), 69 | } 70 | 71 | // ... 72 | 73 | let rust_enum = ocaml_variant.to_rust(); 74 | let new_ocaml_variant = rust_enum.to_ocaml(cr); 75 | ``` 76 | 77 | ### Call OCaml functions from Rust 78 | 79 | ```ocaml 80 | (* OCaml *) 81 | Callback.register "ocaml_print_endline" print_endline 82 | ``` 83 | 84 | ```rust 85 | // Rust 86 | ocaml! { 87 | fn ocaml_print_endline(s: String); 88 | } 89 | 90 | // ... 91 | 92 | ocaml_print_endline(cr, "hello OCaml!"); 93 | ``` 94 | 95 | ### Call Rust functions from OCaml 96 | 97 | ```rust 98 | #[ocaml_interop::export] 99 | pub fn twice_boxed_int(cr: &mut OCamlRuntime, num: OCaml) -> OCaml { 100 | let num = num.to_rust(); 101 | let result = num * 2; 102 | result.to_ocaml(cr) 103 | } 104 | ``` 105 | 106 | ```ocaml 107 | (* OCaml *) 108 | external rust_twice_boxed_int: int64 -> int64 = "twice_boxed_int" 109 | 110 | (* ... *) 111 | 112 | let result = rust_twice_boxed_int 123L in 113 | (* ... *) 114 | ``` 115 | 116 | ## References and links 117 | 118 | - OCaml Manual: [Chapter 20 Interfacing C with OCaml](https://caml.inria.fr/pub/docs/manual-ocaml/intfc.html). 119 | - [Safely Mixing OCaml and Rust](https://docs.google.com/viewer?a=v&pid=sites&srcid=ZGVmYXVsdGRvbWFpbnxtbHdvcmtzaG9wcGV8Z3g6NDNmNDlmNTcxMDk1YTRmNg) paper by Stephen Dolan. 120 | - [Safely Mixing OCaml and Rust](https://www.youtube.com/watch?v=UXfcENNM_ts) talk by Stephen Dolan. 121 | - [CAMLroot: revisiting the OCaml FFI](https://arxiv.org/abs/1812.04905). 122 | - [caml-oxide](https://github.com/stedolan/caml-oxide), the code from that paper. 123 | - [ocaml-rs](https://github.com/zshipko/ocaml-rs), another OCaml<->Rust FFI library. 124 | - [ocaml-boxroot](https://gitlab.com/ocaml-rust/ocaml-boxroot) 125 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | const OCAML_INTEROP_NO_CAML_STARTUP: &str = "OCAML_INTEROP_NO_CAML_STARTUP"; 5 | 6 | fn main() { 7 | println!("cargo:rerun-if-env-changed={OCAML_INTEROP_NO_CAML_STARTUP}",); 8 | if std::env::var(OCAML_INTEROP_NO_CAML_STARTUP).is_ok() { 9 | println!("cargo:rustc-cfg=feature=\"no-caml-startup\""); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ocaml-interop-derive" 3 | version = "0.12.0" 4 | authors = ["Bruno Deferrari "] 5 | description = "Proc-macros for ocaml-interop" 6 | homepage = "https://github.com/tizoc/ocaml-interop" 7 | repository = "https://github.com/tizoc/ocaml-interop" 8 | keywords = ["ocaml", "rust", "ffi", "interop"] 9 | license = "MIT" 10 | edition = "2021" 11 | 12 | [lib] 13 | proc-macro = true 14 | 15 | [dependencies] 16 | syn = { version = "2.0", features = ["full"] } 17 | quote = "1.0" 18 | proc-macro2 = "1.0" 19 | heck = "0.4" 20 | 21 | [dev-dependencies] 22 | pretty_assertions = "1.0" 23 | -------------------------------------------------------------------------------- /derive/src/common/attr_parsing.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use crate::common::{OCamlInteropError, Result}; 5 | use syn::{punctuated::Punctuated, Attribute, Expr, Lit, Meta, Token}; 6 | 7 | /// Struct to hold parsed OCaml attributes from #[ocaml(...)] annotations. 8 | #[derive(Default, Debug)] 9 | pub struct OCamlAttributes { 10 | pub name: Option, 11 | pub as_: Option, 12 | pub tag: Option, 13 | pub polymorphic_variant: bool, 14 | } 15 | 16 | impl OCamlAttributes { 17 | /// Parse a string attribute value from an expression 18 | fn parse_string_attribute(attr_name: &str, expr: &Expr) -> Result { 19 | match expr { 20 | Expr::Lit(expr_lit) => match &expr_lit.lit { 21 | Lit::Str(lit_str) => Ok(lit_str.value()), 22 | _ => Err(OCamlInteropError::attribute_error_spanned( 23 | Some(attr_name), 24 | format!("Expected string literal for '{attr_name}'"), 25 | &expr_lit.lit, 26 | )), 27 | }, 28 | _ => Err(OCamlInteropError::attribute_error_spanned( 29 | Some(attr_name), 30 | format!("Expected literal expression for '{attr_name}'"), 31 | expr, 32 | )), 33 | } 34 | } 35 | 36 | /// Process a name=value attribute and update the attributes struct 37 | fn process_named_attribute( 38 | ocaml_attrs: &mut OCamlAttributes, 39 | name_value: &syn::MetaNameValue, 40 | ) -> Result<()> { 41 | let name = name_value 42 | .path 43 | .get_ident() 44 | .map(|id| id.to_string()) 45 | .unwrap_or_else(|| "unknown".to_string()); 46 | 47 | let value = &name_value.value; 48 | let attr_str = match name.as_str() { 49 | "name" => &mut ocaml_attrs.name, 50 | "as_" => &mut ocaml_attrs.as_, 51 | "tag" => &mut ocaml_attrs.tag, 52 | _ => { 53 | return Err(OCamlInteropError::attribute_error_spanned( 54 | Some(name), 55 | "Unknown attribute in #[ocaml(...)]", 56 | &name_value.path, 57 | )); 58 | } 59 | }; 60 | 61 | *attr_str = Some(Self::parse_string_attribute(&name, value)?); 62 | Ok(()) 63 | } 64 | 65 | /// Parse all OCaml attributes from a list of attributes 66 | pub fn from_attrs(attrs: &[Attribute]) -> Result { 67 | let mut ocaml_attrs = OCamlAttributes::default(); 68 | 69 | // Find and process the #[ocaml(...)] attribute 70 | for attr in attrs { 71 | if !attr.path().is_ident("ocaml") { 72 | continue; 73 | } 74 | 75 | // Parse the comma-separated meta items inside #[ocaml(...)] 76 | let nested_metas = 77 | attr.parse_args_with(Punctuated::::parse_terminated)?; 78 | 79 | // Process each meta item 80 | for nested_meta in nested_metas { 81 | match nested_meta { 82 | // Handle name=value attributes 83 | Meta::NameValue(name_value) => { 84 | Self::process_named_attribute(&mut ocaml_attrs, &name_value)?; 85 | } 86 | 87 | // Handle flag attributes 88 | Meta::Path(path) => { 89 | let name = path 90 | .get_ident() 91 | .map(|id| id.to_string()) 92 | .unwrap_or_else(|| "unknown".to_string()); 93 | 94 | match name.as_str() { 95 | "polymorphic_variant" => { 96 | ocaml_attrs.polymorphic_variant = true; 97 | } 98 | _ => { 99 | return Err(OCamlInteropError::attribute_error_spanned( 100 | Some(name), 101 | "Unknown flag attribute in #[ocaml(...)]", 102 | &path, 103 | )); 104 | } 105 | } 106 | } 107 | 108 | // Handle unsupported attribute formats 109 | _ => { 110 | return Err(OCamlInteropError::attribute_error_spanned( 111 | None::, 112 | "Unsupported attribute format inside #[ocaml(...)]", 113 | &nested_meta, 114 | )); 115 | } 116 | } 117 | } 118 | 119 | // Only process the first #[ocaml(...)] attribute found 120 | return Ok(ocaml_attrs); 121 | } 122 | 123 | // No #[ocaml(...)] attribute found, return default 124 | Ok(ocaml_attrs) 125 | } 126 | 127 | // Simple accessor methods 128 | pub fn get_name(&self) -> &Option { 129 | &self.name 130 | } 131 | pub fn get_tag(&self) -> &Option { 132 | &self.tag 133 | } 134 | pub fn is_polymorphic_variant(&self) -> bool { 135 | self.polymorphic_variant 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /derive/src/common/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use proc_macro2::Span; 5 | use std::fmt; 6 | use syn::spanned::Spanned; 7 | 8 | #[derive(Debug)] 9 | pub enum OCamlInteropError { 10 | Message { message: String, span: Span }, 11 | Syn(syn::Error), 12 | } 13 | 14 | impl OCamlInteropError { 15 | pub fn new(message: impl Into, span: Span) -> Self { 16 | OCamlInteropError::Message { 17 | message: message.into(), 18 | span, 19 | } 20 | } 21 | 22 | pub fn new_spanned(message: impl Into, item: &impl Spanned) -> Self { 23 | Self::new(message, item.span()) 24 | } 25 | 26 | // Specialized errors that format messages appropriately 27 | pub fn type_error_spanned(message: impl Into, item: &impl Spanned) -> Self { 28 | Self::new_spanned(message, item) 29 | } 30 | 31 | pub fn attribute_error_spanned( 32 | name: Option>, 33 | message: impl Into, 34 | item: &impl Spanned, 35 | ) -> Self { 36 | let msg = if let Some(n) = name { 37 | format!("Attribute '{}': {}", n.into(), message.into()) 38 | } else { 39 | message.into() 40 | }; 41 | Self::new_spanned(msg, item) 42 | } 43 | 44 | pub fn validation_error( 45 | message: impl Into, 46 | span: Span, 47 | context: Option>, 48 | ) -> Self { 49 | let msg = if let Some(ctx) = context { 50 | format!("{}: {}", ctx.into(), message.into()) 51 | } else { 52 | message.into() 53 | }; 54 | Self::new(msg, span) 55 | } 56 | 57 | pub fn validation_error_spanned( 58 | message: impl Into, 59 | item: &impl Spanned, 60 | context: Option>, 61 | ) -> Self { 62 | Self::validation_error(message, item.span(), context) 63 | } 64 | 65 | pub fn span(&self) -> Span { 66 | match self { 67 | OCamlInteropError::Message { span, .. } => *span, 68 | OCamlInteropError::Syn(err) => err.span(), 69 | } 70 | } 71 | 72 | pub fn to_syn_error(&self) -> syn::Error { 73 | match self { 74 | OCamlInteropError::Syn(err) => err.clone(), 75 | OCamlInteropError::Message { message, span } => syn::Error::new(*span, message), 76 | } 77 | } 78 | 79 | pub fn to_compile_error(&self) -> proc_macro2::TokenStream { 80 | self.to_syn_error().to_compile_error() 81 | } 82 | 83 | pub fn into_syn_error(self) -> syn::Error { 84 | match self { 85 | OCamlInteropError::Syn(err) => err, 86 | OCamlInteropError::Message { message, span } => syn::Error::new(span, message), 87 | } 88 | } 89 | 90 | // into_err removed as it was just a wrapper around Err 91 | } 92 | 93 | impl fmt::Display for OCamlInteropError { 94 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 95 | match self { 96 | OCamlInteropError::Message { message, .. } => write!(f, "{message}"), 97 | OCamlInteropError::Syn(err) => write!(f, "{err}"), 98 | } 99 | } 100 | } 101 | 102 | impl std::error::Error for OCamlInteropError {} 103 | 104 | impl From for OCamlInteropError { 105 | fn from(err: syn::Error) -> Self { 106 | OCamlInteropError::Syn(err) 107 | } 108 | } 109 | 110 | impl From for syn::Error { 111 | fn from(err: OCamlInteropError) -> Self { 112 | err.to_syn_error() 113 | } 114 | } 115 | 116 | /// A specialized Result type for OCamlInteropDerive 117 | pub type Result = std::result::Result; 118 | -------------------------------------------------------------------------------- /derive/src/common/field_processing.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use proc_macro2::{Span, TokenStream}; 5 | use quote::quote; 6 | use syn::Ident; 7 | 8 | use crate::common::parsing::FieldRep; 9 | 10 | pub fn make_field_var(idx: usize, span: Option) -> Ident { 11 | let span = span.unwrap_or_else(Span::call_site); 12 | Ident::new(&format!("field{idx}"), span) 13 | } 14 | 15 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 16 | pub enum TypeDirection { 17 | ToOCaml, 18 | FromOCaml, 19 | } 20 | 21 | pub fn get_ocaml_type(field_rep: &FieldRep, _direction: TypeDirection) -> TokenStream { 22 | match &field_rep.ocaml_as_type_override_ts { 23 | Some(as_ts) => quote! { #as_ts }, 24 | None => { 25 | let field_type = &field_rep.ty; 26 | quote! { <#field_type as ::ocaml_interop::DefaultOCamlMapping>::OCamlType } 27 | } 28 | } 29 | } 30 | 31 | pub fn generate_field_conversion_and_storage( 32 | field_rep: &FieldRep, 33 | field_access: TokenStream, 34 | var_name: &Ident, 35 | container_expr: TokenStream, 36 | field_idx: usize, 37 | ) -> TokenStream { 38 | let ocaml_type = get_ocaml_type(field_rep, TypeDirection::ToOCaml); 39 | let idx_lit = syn::LitInt::new(&format!("{field_idx}usize"), Span::call_site()); 40 | 41 | quote! { 42 | let #var_name: ::ocaml_interop::OCaml<#ocaml_type> = #field_access.to_ocaml(cr); 43 | unsafe { ::ocaml_interop::internal::store_field(#container_expr, #idx_lit, #var_name.raw()); } 44 | } 45 | } 46 | 47 | pub fn generate_struct_field_extractions( 48 | fields: &[FieldRep], 49 | ocaml_value_expr: TokenStream, 50 | ) -> Vec { 51 | fields 52 | .iter() 53 | .enumerate() 54 | .map(|(idx, field_rep)| { 55 | let var_name = Ident::new(&format!("rust_field_{idx}"), field_rep.span); 56 | let ocaml_type = get_ocaml_type(field_rep, TypeDirection::FromOCaml); 57 | let idx_lit = syn::LitInt::new(&format!("{idx}"), Span::call_site()); 58 | 59 | quote! { 60 | let #var_name = #ocaml_value_expr.field::<#ocaml_type>(#idx_lit).to_rust(); 61 | } 62 | }) 63 | .collect() 64 | } 65 | 66 | pub fn generate_struct_field_conversions( 67 | fields: &[FieldRep], 68 | container_expr: TokenStream, 69 | self_access_pattern: impl Fn(usize, &FieldRep) -> TokenStream, 70 | ) -> Vec { 71 | fields 72 | .iter() 73 | .enumerate() 74 | .map(|(idx, field_rep)| { 75 | let field_access = self_access_pattern(idx, field_rep); 76 | let var_name = if let Some(ident) = &field_rep.ident { 77 | Ident::new(&format!("field_{ident}_ocaml"), field_rep.span) 78 | } else { 79 | Ident::new(&format!("field_{idx}_ocaml"), field_rep.span) 80 | }; 81 | 82 | generate_field_conversion_and_storage( 83 | field_rep, 84 | field_access, 85 | &var_name, 86 | container_expr.clone(), 87 | idx, 88 | ) 89 | }) 90 | .collect() 91 | } 92 | 93 | pub fn create_ocaml_block(size: usize, tag: usize) -> TokenStream { 94 | let size_lit = syn::LitInt::new(&format!("{size}usize"), Span::call_site()); 95 | let tag_lit = syn::LitInt::new(&format!("{tag}"), Span::call_site()); 96 | 97 | quote! { 98 | ::ocaml_interop::BoxRoot::new(unsafe { 99 | ::ocaml_interop::OCaml::new(cr, ::ocaml_interop::internal::caml_alloc(#size_lit, #tag_lit)) 100 | }) 101 | } 102 | } 103 | 104 | pub fn create_ocaml_tuple(size: usize) -> TokenStream { 105 | let size_lit = syn::LitInt::new(&format!("{size}usize"), Span::call_site()); 106 | 107 | quote! { 108 | ::ocaml_interop::BoxRoot::new(unsafe { 109 | ::ocaml_interop::internal::alloc_tuple(cr, #size_lit) 110 | }) 111 | } 112 | } 113 | 114 | pub fn create_polymorphic_variant_block( 115 | polytag_expr: TokenStream, 116 | payload_expr: TokenStream, 117 | ) -> TokenStream { 118 | quote! { 119 | unsafe { 120 | let block = ::ocaml_interop::internal::caml_alloc( 121 | 2usize, // Size is 2: [hash, payload] 122 | ::ocaml_interop::internal::tag::TAG_POLYMORPHIC_VARIANT, 123 | ); 124 | ::ocaml_interop::internal::store_field(block, 0, #polytag_expr); 125 | ::ocaml_interop::internal::store_field(block, 1, #payload_expr); 126 | ::ocaml_interop::OCaml::new(cr, block) 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /derive/src/common/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | pub mod attr_parsing; 5 | pub mod error; 6 | pub mod field_processing; 7 | pub mod parsing; 8 | pub mod polytag_utils; 9 | pub mod validation; 10 | 11 | pub use error::{OCamlInteropError, Result}; 12 | 13 | pub fn format_type(ty: String) -> String { 14 | ty.replace(" <", "<") 15 | .replace("< ", "<") 16 | .replace(" >", ">") 17 | .replace("> ", ">") 18 | .replace(" ,", ",") 19 | } 20 | -------------------------------------------------------------------------------- /derive/src/common/polytag_utils.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use proc_macro2::TokenStream; 5 | use quote::quote; 6 | use syn::Ident; 7 | 8 | /// Generates code to compute a polymorphic variant tag hash value 9 | pub fn generate_polytag_hash(variant_ident: &Ident, tag_str: Option<&String>) -> TokenStream { 10 | let string_to_hash_with_null = match tag_str { 11 | Some(tag_str) => format!("{tag_str}\0"), 12 | None => format!("{variant_ident}\0"), 13 | }; 14 | let static_tag_hash_ident = Ident::new("TAG_HASH", variant_ident.span()); 15 | let static_once_ident = Ident::new("INIT_TAG_HASH", variant_ident.span()); 16 | 17 | quote! {{ 18 | static mut #static_tag_hash_ident: ::ocaml_interop::RawOCaml = 0; 19 | static #static_once_ident: ::std::sync::Once = ::std::sync::Once::new(); 20 | unsafe { 21 | #static_once_ident.call_once(|| { 22 | #static_tag_hash_ident = ::ocaml_interop::internal::caml_hash_variant(#string_to_hash_with_null.as_ptr()); 23 | }); 24 | #static_tag_hash_ident 25 | } 26 | }} 27 | } 28 | -------------------------------------------------------------------------------- /derive/src/common/validation.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use proc_macro2::Span; 5 | 6 | use crate::common::parsing::{EnumKind, FieldRep, TypeRep, TypeRepData, VariantRep}; 7 | use crate::common::{OCamlInteropError, Result}; 8 | 9 | /// Creates an error for when an attribute is used in the wrong context 10 | pub fn invalid_attribute_use( 11 | attribute_name: impl Into, 12 | context: impl Into, 13 | expected_context: impl Into, 14 | span: Span, 15 | ) -> OCamlInteropError { 16 | let attr_name = attribute_name.into(); 17 | OCamlInteropError::validation_error( 18 | format!( 19 | "#[ocaml({attr_name})] is only applicable on {}, but was used on {}.", 20 | expected_context.into(), 21 | context.into() 22 | ), 23 | span, 24 | Some(format!("attribute validation: {attr_name}")), 25 | ) 26 | } 27 | 28 | /// Validates that the `#[ocaml(polymorphic_variant)]` attribute is only used on enums 29 | pub fn validate_polymorphic_kind(type_rep: &TypeRep) -> Result<()> { 30 | if type_rep.attrs.is_polymorphic_variant() 31 | && !matches!(&type_rep.data, TypeRepData::Enum { .. }) 32 | { 33 | return Err(invalid_attribute_use( 34 | "polymorphic_variant".to_string(), 35 | "non-enum type", 36 | "enum types only", 37 | type_rep.ident.span(), 38 | )); 39 | } 40 | Ok(()) 41 | } 42 | 43 | /// Validates that the `#[ocaml(tag="...")]` attribute is only used on polymorphic enum variants 44 | pub fn validate_tag_attribute(variant_rep: &VariantRep, enum_kind: EnumKind) -> Result<()> { 45 | if let Some(tag) = &variant_rep.attrs.tag { 46 | if enum_kind != EnumKind::Polymorphic { 47 | return Err(invalid_attribute_use( 48 | format!("tag=\"{tag}\""), 49 | format!("variant '{}' in a regular enum", variant_rep.ident), 50 | "variants of a polymorphic enum (i.e. enum has #[ocaml(polymorphic_variant)])", 51 | variant_rep.span, 52 | )); 53 | } 54 | } 55 | Ok(()) 56 | } 57 | 58 | /// Validates field attributes 59 | pub fn validate_field_attributes(field_rep: &FieldRep) -> Result<()> { 60 | // Check that as_ attribute was successfully parsed if present 61 | if field_rep.ocaml_as_type_override_ts.is_none() && field_rep.attrs.as_.is_some() { 62 | // This implies `as_` was present but failed to parse to TokenStream. 63 | // Error should have been generated during parse_field already. 64 | } 65 | 66 | // Additional field validation could be added here 67 | 68 | Ok(()) 69 | } 70 | 71 | /// Validates that enums have at least one variant 72 | pub fn validate_enum_has_variants(type_rep: &TypeRep) -> Result<()> { 73 | if let TypeRepData::Enum { variants, .. } = &type_rep.data { 74 | if variants.is_empty() { 75 | return Err(OCamlInteropError::validation_error( 76 | "Empty enums are not supported in OCaml interop".to_string(), 77 | type_rep.ident.span(), 78 | Some("enum must have at least one variant".to_string()), 79 | )); 80 | } 81 | } 82 | Ok(()) 83 | } 84 | 85 | /// General validation for a type representation 86 | pub fn validate_type_rep(type_rep: &TypeRep) -> Result<()> { 87 | // 1. Validate container-level attributes 88 | validate_polymorphic_kind(type_rep)?; 89 | 90 | // 2. Validate that enums have variants 91 | validate_enum_has_variants(type_rep)?; 92 | 93 | // 3. Validate type-specific elements 94 | match &type_rep.data { 95 | TypeRepData::Enum { variants, kind, .. } => { 96 | for variant in variants { 97 | validate_tag_attribute(variant, *kind)?; 98 | 99 | for field in &variant.fields { 100 | validate_field_attributes(field)?; 101 | } 102 | } 103 | } 104 | TypeRepData::Struct { fields } => { 105 | for field in fields { 106 | validate_field_attributes(field)?; 107 | } 108 | } 109 | } 110 | 111 | Ok(()) 112 | } 113 | -------------------------------------------------------------------------------- /derive/src/export/core.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use quote::quote; 5 | use syn::Pat; 6 | use syn::Type; 7 | 8 | // Holds all parsed data from the macro input, used to feed the expansion phase. 9 | pub(crate) struct ExportedFnData { 10 | // Attributes 11 | pub(crate) bytecode_fn_name_opt: Option, 12 | 13 | pub(crate) no_panic_catch: bool, 14 | pub(crate) noalloc: bool, 15 | 16 | // Original function elements 17 | pub(crate) native_fn_name: syn::Ident, // Name of the original Rust function, used for the extern "C" native fn 18 | pub(crate) visibility: syn::Visibility, // Original visibility, for bytecode stub 19 | pub(crate) original_fn_block: Box, // The original function's body 20 | pub(crate) is_async: bool, 21 | pub(crate) has_generics: bool, 22 | pub(crate) is_variadic: bool, 23 | pub(crate) abi: Option, 24 | 25 | // Runtime argument details 26 | pub(crate) runtime_arg_pat: Box, 27 | pub(crate) runtime_arg_ty: Box, 28 | 29 | // Processed user-provided arguments (details for C signature, Rust initialization, type info) 30 | pub(crate) processed_args: Vec, // User args, excluding runtime 31 | 32 | // Return type processing results 33 | pub(crate) return_interop_detail: InteropTypeDetail, // InteropTypeDetail of the user's function return type 34 | pub(crate) user_return_type_ast: syn::Type, // The syn::Type of the original function's return 35 | 36 | // Spans for error reporting 37 | pub(crate) original_fn_ident_span: proc_macro2::Span, 38 | } 39 | 40 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 41 | pub(crate) enum PrimitiveInteropType { 42 | F64, 43 | I64, 44 | I32, 45 | Bool, 46 | ISize, 47 | } 48 | 49 | #[derive(Clone)] 50 | pub(crate) enum InteropTypeDetail { 51 | Unit, // Represents Rust's () type, primarily for return types 52 | Primitive { 53 | primitive_type: PrimitiveInteropType, 54 | original_rust_type: Box, // e.g., syn::Type for "f64" 55 | }, 56 | OCaml { 57 | wrapper_type: Box, // e.g., syn::Type for "OCaml" 58 | inner_type: Box, // e.g., syn::Type for "String" 59 | }, 60 | BoxRoot { 61 | // Only for arguments 62 | inner_type: Box, // e.g., syn::Type for "MyStruct" 63 | }, 64 | } 65 | 66 | impl InteropTypeDetail { 67 | pub(crate) fn is_primitive(&self) -> bool { 68 | matches!(self, InteropTypeDetail::Primitive { .. }) 69 | } 70 | 71 | pub(crate) fn get_ocaml_to_rust_fn_name(&self) -> Result { 72 | match self { 73 | InteropTypeDetail::Primitive { primitive_type, .. } => Ok(match primitive_type { 74 | PrimitiveInteropType::F64 => "float_val".to_string(), 75 | PrimitiveInteropType::I64 => "int64_val".to_string(), 76 | PrimitiveInteropType::I32 => "int32_val".to_string(), 77 | PrimitiveInteropType::Bool => "bool_val".to_string(), 78 | PrimitiveInteropType::ISize => "int_val".to_string(), 79 | }), 80 | _ => Err(syn::Error::new( 81 | // This function should ideally only be called on primitives. 82 | // A more specific span could be used if available from the caller context. 83 | proc_macro2::Span::call_site(), 84 | "Internal error: get_ocaml_to_rust_fn_name called on non-primitive type", 85 | )), 86 | } 87 | } 88 | 89 | pub(crate) fn get_rust_to_ocaml_fn_name(&self) -> Result { 90 | match self { 91 | InteropTypeDetail::Primitive { primitive_type, .. } => Ok(match primitive_type { 92 | PrimitiveInteropType::F64 => "alloc_float".to_string(), 93 | PrimitiveInteropType::I64 => "alloc_int64".to_string(), 94 | PrimitiveInteropType::I32 => "alloc_int32".to_string(), 95 | PrimitiveInteropType::Bool => "make_ocaml_bool".to_string(), 96 | PrimitiveInteropType::ISize => "make_ocaml_int".to_string(), 97 | }), 98 | _ => Err(syn::Error::new( 99 | // This function should ideally only be called on primitives. 100 | // A more specific span could be used if available from the caller context. 101 | proc_macro2::Span::call_site(), 102 | "Internal error: get_rust_to_ocaml_fn_name called on non-primitive type", 103 | )), 104 | } 105 | } 106 | 107 | pub(crate) fn get_conversion_module_path_tokens(&self) -> proc_macro2::TokenStream { 108 | // Path for primitive type conversion functions. 109 | quote! { ::ocaml_interop::internal:: } 110 | } 111 | } 112 | 113 | // Holds processed information for a single user-provided function argument. 114 | pub(crate) struct ProcessedArg { 115 | pub(crate) pattern: Box, 116 | pub(crate) type_detail: InteropTypeDetail, 117 | pub(crate) original_rust_type: Box, 118 | } 119 | -------------------------------------------------------------------------------- /derive/src/export/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | pub mod codegen; 5 | pub mod core; 6 | pub mod parsing; 7 | pub mod validation; 8 | -------------------------------------------------------------------------------- /derive/src/export/parsing.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use syn::parse::Parser; 5 | use syn::punctuated::Punctuated; 6 | use syn::spanned::Spanned; 7 | use syn::{FnArg, ItemFn, Meta, Token}; 8 | 9 | use crate::export::core::{ExportedFnData, ProcessedArg}; 10 | // Import shared parsing utilities from common 11 | use crate::common::parsing::{ 12 | check_duplicate_attr, parse_string_literal_attribute, process_extern_argument, 13 | process_flag_attribute, process_return_type, 14 | }; 15 | 16 | /// Parse export attributes like bytecode, no_panic_catch, noalloc 17 | fn parse_export_attributes( 18 | attr_ts: proc_macro2::TokenStream, 19 | ) -> Result<(Option, bool, bool), syn::Error> { 20 | let mut bytecode_fn_name_opt: Option = None; 21 | let mut no_panic_catch = false; 22 | let mut noalloc = false; 23 | 24 | // Keep track of spans only for error reporting during parsing 25 | let mut bytecode_meta_span: Option = None; 26 | let mut no_panic_catch_span: Option = None; 27 | let mut noalloc_span: Option = None; 28 | 29 | let attribute_parser = Punctuated::::parse_terminated; 30 | let parsed_attributes = attribute_parser.parse2(attr_ts)?; 31 | 32 | for meta in parsed_attributes { 33 | if meta.path().is_ident("bytecode") { 34 | // Check for duplicate attribute 35 | check_duplicate_attr("bytecode", meta.path(), &bytecode_meta_span)?; 36 | 37 | // Process the bytecode attribute 38 | match meta { 39 | syn::Meta::NameValue(mnv) => { 40 | let bytecode_name = parse_string_literal_attribute(&mnv, "bytecode")?; 41 | bytecode_fn_name_opt = Some(syn::Ident::new(&bytecode_name, mnv.path.span())); 42 | bytecode_meta_span = Some(mnv.path.span()); 43 | } 44 | _ => { 45 | return Err(syn::Error::new_spanned( 46 | meta, 47 | "'bytecode' attribute must be a name-value pair (e.g., bytecode = \"my_func_byte\")" 48 | )); 49 | } 50 | } 51 | } else if meta.path().is_ident("no_panic_catch") { 52 | // Check for duplicate attribute 53 | check_duplicate_attr("no_panic_catch", meta.path(), &no_panic_catch_span)?; 54 | 55 | // Process the no_panic_catch flag attribute 56 | let (value, span) = process_flag_attribute(&meta, "no_panic_catch")?; 57 | no_panic_catch = value; 58 | no_panic_catch_span = span; 59 | } else if meta.path().is_ident("noalloc") { 60 | // Check for duplicate attribute 61 | check_duplicate_attr("noalloc", meta.path(), &noalloc_span)?; 62 | 63 | // Process the noalloc flag attribute 64 | let (value, span) = process_flag_attribute(&meta, "noalloc")?; 65 | noalloc = value; 66 | noalloc_span = span; 67 | } else { 68 | return Err(syn::Error::new_spanned(meta.path(), format!( 69 | "unsupported attribute '{}', only 'bytecode', 'no_panic_catch' and 'noalloc' are supported", 70 | meta.path().get_ident().map_or_else(|| String::from("?"), |i| i.to_string()) 71 | ))); 72 | } 73 | } 74 | 75 | Ok((bytecode_fn_name_opt, no_panic_catch, noalloc)) 76 | } 77 | 78 | /// Main entry point for parsing export function definitions 79 | pub(crate) fn parse_export_definition( 80 | attr_ts: proc_macro2::TokenStream, 81 | input_fn: &ItemFn, 82 | ) -> Result { 83 | let (bytecode_fn_name_opt, no_panic_catch, noalloc) = parse_export_attributes(attr_ts)?; 84 | 85 | let native_fn_name = input_fn.sig.ident.clone(); 86 | let visibility = input_fn.vis.clone(); 87 | let function_body = input_fn.block.clone(); 88 | let is_async = input_fn.sig.asyncness.is_some(); 89 | let has_generics = !input_fn.sig.generics.params.is_empty(); 90 | let is_variadic = input_fn.sig.variadic.is_some(); 91 | let abi = input_fn.sig.abi.clone(); 92 | 93 | let fn_inputs = &input_fn.sig.inputs; 94 | let original_fn_return_type_ast = &input_fn.sig.output; 95 | let fn_inputs_span = fn_inputs.span(); 96 | let original_fn_ident_span = input_fn.sig.ident.span(); 97 | 98 | let mut original_fn_args_iter = fn_inputs.iter(); 99 | 100 | let fn_arg = original_fn_args_iter.next().ok_or_else(|| { 101 | syn::Error::new( 102 | fn_inputs_span, 103 | "Exported functions must take an OCamlRuntime as their first argument.", 104 | ) 105 | })?; 106 | 107 | let (runtime_arg_pat, runtime_arg_ty) = match fn_arg { 108 | FnArg::Typed(pt) => (Box::new(pt.pat.as_ref().clone()), pt.ty.clone()), 109 | FnArg::Receiver(rec) => { 110 | return Err(syn::Error::new_spanned( 111 | rec, 112 | "OCamlRuntime argument cannot be a receiver (self).", 113 | )) 114 | } 115 | }; 116 | 117 | let mut processed_args: Vec = Vec::new(); 118 | for arg in original_fn_args_iter { 119 | processed_args.push(process_extern_argument(arg)?); 120 | } 121 | 122 | let (return_interop_detail, user_return_type_ast) = 123 | process_return_type(original_fn_return_type_ast)?; 124 | 125 | Ok(ExportedFnData { 126 | bytecode_fn_name_opt, 127 | no_panic_catch, 128 | noalloc, 129 | native_fn_name, 130 | visibility, 131 | original_fn_block: function_body, 132 | runtime_arg_pat, 133 | runtime_arg_ty, 134 | processed_args, 135 | return_interop_detail, 136 | user_return_type_ast, 137 | is_async, 138 | has_generics, 139 | is_variadic, 140 | abi, 141 | original_fn_ident_span, 142 | }) 143 | } 144 | -------------------------------------------------------------------------------- /derive/src/export/validation.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use quote::quote; 5 | use syn::{Error, Type}; 6 | 7 | use crate::export::core::{ExportedFnData, InteropTypeDetail}; 8 | 9 | /// Validates that the runtime argument is a proper OCamlRuntime reference 10 | fn validate_runtime_arg(data: &ExportedFnData) -> Result<(), Error> { 11 | match &*data.runtime_arg_ty { 12 | Type::Reference(type_ref) => { 13 | // Check if the referenced type is OCamlRuntime 14 | if let Type::Path(type_path) = &*type_ref.elem { 15 | let path_str = quote!(#type_path).to_string(); 16 | if !(path_str == "OCamlRuntime" 17 | || path_str == "ocaml_interop :: OCamlRuntime" 18 | || path_str == ":: ocaml_interop :: OCamlRuntime") 19 | { 20 | return Err(Error::new_spanned( 21 | &data.runtime_arg_ty, 22 | "Exported functions must take an OCamlRuntime reference (e.g., `rt: &OCamlRuntime` or `rt: &mut OCamlRuntime`) as their first argument.", 23 | )); 24 | } 25 | } else { 26 | // Referenced type is not a path, so not OCamlRuntime 27 | return Err(Error::new_spanned( 28 | &data.runtime_arg_ty, 29 | "Exported functions must take an OCamlRuntime reference (e.g., `rt: &OCamlRuntime` or `rt: &mut OCamlRuntime`) as their first argument.", 30 | )); 31 | } 32 | 33 | // Validate OCamlRuntime argument mutability based on noalloc 34 | let is_mutable_runtime = type_ref.mutability.is_some(); 35 | validate_runtime_mutability(data, is_mutable_runtime)?; 36 | 37 | Ok(()) 38 | } 39 | _ => { 40 | // Not a reference type at all 41 | Err(Error::new_spanned( 42 | &data.runtime_arg_ty, 43 | "Exported functions must take an OCamlRuntime reference (e.g., `rt: &OCamlRuntime` or `rt: &mut OCamlRuntime`) as their first argument.", 44 | )) 45 | } 46 | } 47 | } 48 | 49 | /// Validates that the mutability of OCamlRuntime is consistent with noalloc 50 | fn validate_runtime_mutability( 51 | data: &ExportedFnData, 52 | is_mutable_runtime: bool, 53 | ) -> Result<(), Error> { 54 | if data.noalloc { 55 | if is_mutable_runtime { 56 | Err(Error::new_spanned( 57 | &data.runtime_arg_ty, 58 | "When `noalloc` is used, OCaml runtime argument must be an immutable reference (e.g., &OCamlRuntime)", 59 | )) 60 | } else { 61 | Ok(()) 62 | } 63 | } else { 64 | // Default case (allocations allowed, noalloc is false) 65 | if !is_mutable_runtime { 66 | Err(Error::new_spanned( 67 | &data.runtime_arg_ty, 68 | "OCaml runtime argument must be a mutable reference (e.g., &mut OCamlRuntime). Use `#[export(noalloc)]` for an immutable reference.", 69 | )) 70 | } else { 71 | Ok(()) 72 | } 73 | } 74 | } 75 | 76 | // Performs validation checks on the fully parsed ExportedFnData. 77 | pub fn validate_parsed_data(data: &ExportedFnData) -> Result<(), Error> { 78 | validate_runtime_arg(data)?; 79 | validate_argument_constraints(data)?; 80 | validate_return_type(data)?; 81 | validate_function_properties(data)?; 82 | 83 | Ok(()) 84 | } 85 | 86 | /// Validates argument constraints - minimum count and allowed types 87 | fn validate_argument_constraints(data: &ExportedFnData) -> Result<(), Error> { 88 | // Validate that there is at least one non-runtime argument 89 | if data.processed_args.is_empty() { 90 | return Err(syn::Error::new( 91 | data.original_fn_ident_span, // Using the function name's span for this error 92 | "OCaml functions must take at least one argument in addition to the OCamlRuntime.", 93 | )); 94 | } 95 | 96 | // Validate that Unit type `()` is not used as an argument type 97 | for arg in &data.processed_args { 98 | if let InteropTypeDetail::Unit = &arg.type_detail { 99 | return Err(Error::new_spanned( 100 | &arg.original_rust_type, 101 | "Unit type `()` is not a supported argument type directly. Use OCaml<()> if needed for placeholder.", 102 | )); 103 | } 104 | } 105 | 106 | // Additional constraints for noalloc functions 107 | if data.noalloc { 108 | validate_noalloc_arguments(data)?; 109 | } 110 | 111 | Ok(()) 112 | } 113 | 114 | /// Validates arguments specific to noalloc functions 115 | fn validate_noalloc_arguments(data: &ExportedFnData) -> Result<(), Error> { 116 | // Disallow BoxRoot arguments when noalloc is used 117 | for arg in &data.processed_args { 118 | if let InteropTypeDetail::BoxRoot { .. } = &arg.type_detail { 119 | return Err(Error::new_spanned( 120 | &arg.original_rust_type, 121 | "`BoxRoot` arguments are not allowed when `noalloc` is used because BoxRoot implies allocation for rooting.", 122 | )); 123 | } 124 | } 125 | 126 | Ok(()) 127 | } 128 | 129 | /// Validates return type constraints 130 | fn validate_return_type(data: &ExportedFnData) -> Result<(), Error> { 131 | // Validate that BoxRoot is not used as a return type 132 | if let InteropTypeDetail::BoxRoot { .. } = &data.return_interop_detail { 133 | return Err(Error::new_spanned( 134 | &data.user_return_type_ast, // Span of the user's return type 135 | "BoxRoot cannot be used as a return type directly. Return OCaml instead.", 136 | )); 137 | } 138 | 139 | Ok(()) 140 | } 141 | 142 | /// Validates general function properties like visibility, async status, etc. 143 | fn validate_function_properties(data: &ExportedFnData) -> Result<(), Error> { 144 | // Validate function visibility (must be public) 145 | if !matches!(data.visibility, syn::Visibility::Public(_)) { 146 | return Err(Error::new_spanned( 147 | &data.native_fn_name, // Span of the function name 148 | "Exported functions must be public (`pub`).", 149 | )); 150 | } 151 | 152 | // Validate function is not async 153 | if data.is_async { 154 | return Err(Error::new_spanned( 155 | &data.native_fn_name, 156 | "Exported functions cannot be `async`.", 157 | )); 158 | } 159 | 160 | // Validate function is not generic 161 | if data.has_generics { 162 | return Err(Error::new_spanned( 163 | &data.native_fn_name, 164 | "Exported functions cannot have generic parameters.", 165 | )); 166 | } 167 | 168 | // Validate function is not variadic 169 | if data.is_variadic { 170 | return Err(Error::new_spanned( 171 | &data.native_fn_name, 172 | "Exported functions cannot be variadic (e.g., `...`).", 173 | )); 174 | } 175 | 176 | // Validate ABI 177 | validate_function_abi(data)?; 178 | 179 | Ok(()) 180 | } 181 | 182 | /// Validates the function's ABI 183 | fn validate_function_abi(data: &ExportedFnData) -> Result<(), Error> { 184 | if let Some(abi) = &data.abi { 185 | if let Some(name) = &abi.name { 186 | let abi_name = name.value(); 187 | if abi_name != "C" && abi_name != "C-unwind" { 188 | return Err(Error::new_spanned( 189 | abi, 190 | "Exported functions must use `extern \"C\"` or `extern \"C-unwind\"` ABI. Other ABIs are not supported.", 191 | )); 192 | } 193 | } 194 | } 195 | 196 | Ok(()) 197 | } 198 | -------------------------------------------------------------------------------- /derive/src/from_ocaml/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | pub mod codegen; 5 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | extern crate proc_macro; 5 | use proc_macro::TokenStream; 6 | use syn::ItemFn; 7 | 8 | mod common; 9 | mod export; 10 | mod from_ocaml; 11 | mod ocaml_describer; 12 | mod to_ocaml; 13 | 14 | #[cfg(test)] 15 | mod tests { 16 | mod export_tests; 17 | mod from_ocaml_tests; 18 | mod ocaml_describer_tests; 19 | mod to_ocaml_tests; 20 | mod to_ocaml_type_safety_tests; 21 | } 22 | 23 | fn export_internal_logic( 24 | attr_ts: proc_macro2::TokenStream, 25 | item_ts: proc_macro2::TokenStream, 26 | ) -> Result { 27 | let input_fn = syn::parse2::(item_ts.clone()).map_err(|e| { 28 | syn::Error::new( 29 | e.span(), 30 | format!("Failed to parse input item as a function: {e}. Input was: {item_ts}",), 31 | ) 32 | })?; 33 | 34 | let parsed_data = export::parsing::parse_export_definition(attr_ts, &input_fn)?; 35 | 36 | export::validation::validate_parsed_data(&parsed_data)?; 37 | 38 | export::codegen::expand_function_from_data(&parsed_data) 39 | } 40 | 41 | // --- Proc Macro Entry Point --- 42 | #[proc_macro_attribute] 43 | pub fn export(attr: TokenStream, item: TokenStream) -> TokenStream { 44 | let attr_ts2 = proc_macro2::TokenStream::from(attr); 45 | let item_ts2 = proc_macro2::TokenStream::from(item); 46 | 47 | match export_internal_logic(attr_ts2, item_ts2) { 48 | Ok(generated_code) => TokenStream::from(generated_code), 49 | Err(err) => TokenStream::from(err.to_compile_error()), 50 | } 51 | } 52 | 53 | #[proc_macro_derive(OCamlDescriber, attributes(ocaml))] 54 | pub fn ocaml_describer_derive(input: TokenStream) -> TokenStream { 55 | // Convert proc_macro::TokenStream to proc_macro2::TokenStream for internal use 56 | let input_pm2 = proc_macro2::TokenStream::from(input); 57 | ocaml_describer::codegen::expand_ocaml_describer(input_pm2) 58 | .unwrap_or_else(|err| err.to_compile_error()) 59 | .into() // Convert proc_macro2::TokenStream back to proc_macro::TokenStream 60 | } 61 | 62 | #[proc_macro_derive(ToOCaml, attributes(ocaml))] 63 | pub fn to_ocaml_derive(input: TokenStream) -> TokenStream { 64 | // Convert proc_macro::TokenStream to proc_macro2::TokenStream for internal use 65 | let input_pm2 = proc_macro2::TokenStream::from(input); 66 | to_ocaml::codegen::expand_to_ocaml(input_pm2) 67 | .unwrap_or_else(|err| err.to_compile_error()) 68 | .into() // Convert proc_macro2::TokenStream back to proc_macro::TokenStream 69 | } 70 | 71 | #[proc_macro_derive(FromOCaml, attributes(ocaml))] 72 | pub fn from_ocaml_derive(input: TokenStream) -> TokenStream { 73 | // Convert proc_macro::TokenStream to proc_macro2::TokenStream for internal use 74 | let input_pm2 = proc_macro2::TokenStream::from(input); 75 | from_ocaml::codegen::expand_from_ocaml(input_pm2) 76 | .unwrap_or_else(|err| err.to_compile_error()) 77 | .into() // Convert proc_macro2::TokenStream back to proc_macro::TokenStream 78 | } 79 | -------------------------------------------------------------------------------- /derive/src/ocaml_describer/codegen.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use heck::ToSnakeCase; 5 | use proc_macro2::TokenStream; 6 | use quote::quote; 7 | use syn::{DeriveInput, Result}; 8 | 9 | use crate::common::attr_parsing::OCamlAttributes; 10 | 11 | /// Add OCamlDescriber trait bound to a where clause for generic type parameters 12 | fn add_ocaml_describer_bounds( 13 | generics: &syn::Generics, 14 | mut where_clause: syn::WhereClause, 15 | ) -> syn::WhereClause { 16 | for param in generics.type_params() { 17 | let param_ident = ¶m.ident; 18 | where_clause 19 | .predicates 20 | .push(syn::parse_quote! { #param_ident: ocaml_interop::OCamlDescriber }); 21 | } 22 | where_clause 23 | } 24 | 25 | /// Generate the OCaml type name code based on generic parameters 26 | fn generate_type_name_code(ocaml_type_name_str: &str, generics: &syn::Generics) -> TokenStream { 27 | let param_count = generics.type_params().count(); 28 | 29 | match param_count { 30 | 0 => quote! { #ocaml_type_name_str.to_string() }, 31 | 32 | 1 => { 33 | // Single parameter case: "param_name type_name" 34 | let param_ident = &generics.type_params().next().unwrap().ident; 35 | quote! { 36 | format!( 37 | "{} {}", 38 | <#param_ident as ocaml_interop::OCamlDescriber>::ocaml_type_name(), 39 | #ocaml_type_name_str 40 | ) 41 | } 42 | } 43 | 44 | _ => { 45 | // Multiple parameters case: "(param1_name, param2_name, ...) type_name" 46 | let param_name_calls = generics.type_params().map(|param| { 47 | let param_ident = ¶m.ident; 48 | quote! { <#param_ident as ocaml_interop::OCamlDescriber>::ocaml_type_name() } 49 | }); 50 | 51 | quote! { 52 | format!("({}) {}", 53 | vec![#(#param_name_calls),*].join(", "), 54 | #ocaml_type_name_str 55 | ) 56 | } 57 | } 58 | } 59 | } 60 | 61 | pub fn expand_ocaml_describer(input: TokenStream) -> Result { 62 | let derive_input = syn::parse2::(input)?; 63 | let type_ident = &derive_input.ident; 64 | 65 | // Parse #[ocaml(...)] attributes on the type 66 | let type_attrs = OCamlAttributes::from_attrs(&derive_input.attrs)?; 67 | 68 | // Get OCaml type name from attribute or convert from Rust name 69 | let ocaml_type_name_str = type_attrs 70 | .get_name() 71 | .clone() 72 | .unwrap_or_else(|| type_ident.to_string().to_snake_case()); 73 | 74 | // Handle generics 75 | let generics = &derive_input.generics; 76 | let (impl_generics, ty_generics, where_clause) = generics.split_for_impl(); 77 | 78 | // Add OCamlDescriber bound to generic type parameters 79 | let where_clause = add_ocaml_describer_bounds( 80 | generics, 81 | where_clause 82 | .cloned() 83 | .unwrap_or_else(|| syn::parse_quote! { where }), 84 | ); 85 | 86 | // Generate the implementation 87 | let final_ocaml_name_code = generate_type_name_code(&ocaml_type_name_str, generics); 88 | 89 | let expanded = quote! { 90 | impl #impl_generics ocaml_interop::OCamlDescriber for #type_ident #ty_generics #where_clause { 91 | fn ocaml_type_name() -> String { 92 | #final_ocaml_name_code 93 | } 94 | } 95 | }; 96 | 97 | Ok(expanded) 98 | } 99 | -------------------------------------------------------------------------------- /derive/src/ocaml_describer/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | pub mod codegen; 5 | -------------------------------------------------------------------------------- /derive/src/tests/ocaml_describer_tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use crate::ocaml_describer::codegen::expand_ocaml_describer; 5 | use pretty_assertions::assert_eq; 6 | use quote::quote; 7 | 8 | #[test] 9 | fn test_ocaml_describer_simple_struct_default_name() { 10 | let input_struct = quote! { 11 | struct MySimpleStruct; 12 | }; 13 | let expected_impl = quote! { 14 | impl ocaml_interop::OCamlDescriber for MySimpleStruct { 15 | fn ocaml_type_name() -> String { 16 | "my_simple_struct".to_string() 17 | } 18 | } 19 | }; 20 | 21 | let actual_impl_result = expand_ocaml_describer(input_struct); 22 | assert!( 23 | actual_impl_result.is_ok(), 24 | "OCamlDescriber expansion failed: {:?}", 25 | actual_impl_result.as_ref().err().map(|e| e.to_string()) 26 | ); 27 | let actual_impl = actual_impl_result.unwrap(); 28 | 29 | assert_eq!(actual_impl.to_string(), expected_impl.to_string()); 30 | } 31 | 32 | #[test] 33 | fn test_ocaml_describer_simple_struct_custom_name() { 34 | let input_struct = quote! { 35 | #[ocaml(name = "custom_name_for_struct")] 36 | struct MyRenamedStruct; 37 | }; 38 | let expected_impl = quote! { 39 | impl ocaml_interop::OCamlDescriber for MyRenamedStruct { 40 | fn ocaml_type_name() -> String { 41 | "custom_name_for_struct".to_string() 42 | } 43 | } 44 | }; 45 | 46 | let actual_impl_result = expand_ocaml_describer(input_struct); 47 | assert!( 48 | actual_impl_result.is_ok(), 49 | "OCamlDescriber expansion failed: {:?}", 50 | actual_impl_result.as_ref().err().map(|e| e.to_string()) 51 | ); 52 | let actual_impl = actual_impl_result.unwrap(); 53 | 54 | assert_eq!(actual_impl.to_string(), expected_impl.to_string()); 55 | } 56 | 57 | #[test] 58 | fn test_ocaml_describer_generic_struct_one_param() { 59 | let input_struct = quote! { 60 | struct MyGenericStruct; 61 | }; 62 | let expected_impl = quote! { 63 | impl ocaml_interop::OCamlDescriber for MyGenericStruct 64 | where A: ocaml_interop::OCamlDescriber 65 | { 66 | fn ocaml_type_name() -> String { 67 | format!("{} {}", ::ocaml_type_name(), "my_generic_struct") 68 | } 69 | } 70 | }; 71 | 72 | let actual_impl_result = expand_ocaml_describer(input_struct); 73 | assert!( 74 | actual_impl_result.is_ok(), 75 | "OCamlDescriber expansion failed: {:?}", 76 | actual_impl_result.as_ref().err().map(|e| e.to_string()) 77 | ); 78 | let actual_impl = actual_impl_result.unwrap(); 79 | 80 | assert_eq!(actual_impl.to_string(), expected_impl.to_string()); 81 | } 82 | 83 | #[test] 84 | fn test_ocaml_describer_generic_struct_multiple_params() { 85 | let input_struct = quote! { 86 | struct MyMultiGeneric; 87 | }; 88 | let expected_impl = quote! { 89 | impl ocaml_interop::OCamlDescriber for MyMultiGeneric 90 | where 91 | A: ocaml_interop::OCamlDescriber, 92 | B: ocaml_interop::OCamlDescriber 93 | { 94 | fn ocaml_type_name() -> String { 95 | format!("({}) {}", 96 | vec![ 97 | ::ocaml_type_name(), 98 | ::ocaml_type_name() 99 | ].join(", "), 100 | "my_multi_generic" 101 | ) 102 | } 103 | } 104 | }; 105 | 106 | let actual_impl_result = expand_ocaml_describer(input_struct); 107 | assert!( 108 | actual_impl_result.is_ok(), 109 | "OCamlDescriber expansion failed: {:?}", 110 | actual_impl_result.as_ref().err().map(|e| e.to_string()) 111 | ); 112 | let actual_impl = actual_impl_result.unwrap(); 113 | 114 | assert_eq!(actual_impl.to_string(), expected_impl.to_string()); 115 | } 116 | 117 | #[test] 118 | fn test_ocaml_describer_generic_struct_existing_where_clause() { 119 | let input_struct = quote! { 120 | struct MyComplexGeneric where T: Clone; 121 | }; 122 | let expected_impl = quote! { 123 | impl ocaml_interop::OCamlDescriber for MyComplexGeneric 124 | where 125 | T: Clone, 126 | T: ocaml_interop::OCamlDescriber 127 | { 128 | fn ocaml_type_name() -> String { 129 | format!("{} {}", ::ocaml_type_name(), "my_complex_generic") 130 | } 131 | } 132 | }; 133 | 134 | let actual_impl_result = expand_ocaml_describer(input_struct); 135 | assert!( 136 | actual_impl_result.is_ok(), 137 | "OCamlDescriber expansion failed: {:?}", 138 | actual_impl_result.as_ref().err().map(|e| e.to_string()) 139 | ); 140 | let actual_impl = actual_impl_result.unwrap(); 141 | 142 | assert_eq!(actual_impl.to_string(), expected_impl.to_string()); 143 | } 144 | 145 | #[test] 146 | fn test_ocaml_describer_lifetime_and_type_param() { 147 | let input_struct = quote! { 148 | struct MyLifetimeStruct<'a, T> { 149 | data: &'a T, 150 | } 151 | }; 152 | let expected_impl = quote! { 153 | impl<'a, T> ocaml_interop::OCamlDescriber for MyLifetimeStruct<'a, T> 154 | where T: ocaml_interop::OCamlDescriber 155 | { 156 | fn ocaml_type_name() -> String { 157 | format!("{} {}", ::ocaml_type_name(), "my_lifetime_struct") 158 | } 159 | } 160 | }; 161 | 162 | let actual_impl_result = expand_ocaml_describer(input_struct); 163 | assert!( 164 | actual_impl_result.is_ok(), 165 | "OCamlDescriber expansion failed: {:?}", 166 | actual_impl_result.as_ref().err().map(|e| e.to_string()) 167 | ); 168 | let actual_impl = actual_impl_result.unwrap(); 169 | 170 | assert_eq!(actual_impl.to_string(), expected_impl.to_string()); 171 | } 172 | -------------------------------------------------------------------------------- /derive/src/tests/to_ocaml_type_safety_tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use crate::common::parsing::{parse_input, EnumKind, VariantKind}; 5 | use crate::to_ocaml::codegen::expand_to_ocaml; 6 | use pretty_assertions::assert_eq; 7 | use quote::quote; 8 | 9 | #[test] 10 | fn test_struct_with_type_safe_components() { 11 | let input_struct = quote! { 12 | struct MySimpleStruct { 13 | #[ocaml(as_ = "OCamlInt")] 14 | a: i32, 15 | b: String, 16 | c: i64, 17 | } 18 | }; 19 | 20 | let derive_input = syn::parse2(input_struct.clone()).unwrap(); 21 | let type_rep = parse_input(derive_input).unwrap(); 22 | 23 | // Check that we correctly parsed the struct 24 | match &type_rep.data { 25 | crate::common::parsing::TypeRepData::Struct { fields } => { 26 | assert_eq!(fields.len(), 3, "Should have 3 fields"); 27 | assert_eq!(fields[0].ident.as_ref().unwrap().to_string(), "a"); 28 | assert_eq!(fields[1].ident.as_ref().unwrap().to_string(), "b"); 29 | assert_eq!(fields[2].ident.as_ref().unwrap().to_string(), "c"); 30 | } 31 | _ => panic!("Expected struct type"), 32 | } 33 | 34 | // Now check that it expands correctly 35 | let actual_impl_result = expand_to_ocaml(input_struct); 36 | assert!( 37 | actual_impl_result.is_ok(), 38 | "expand_to_ocaml failed for simple struct: {:?}", 39 | actual_impl_result.err().map(|e| e.to_string()) 40 | ); 41 | 42 | // Extract the implementation 43 | let actual_impl = actual_impl_result.unwrap(); 44 | 45 | // Just check that it contains the key parts we expect 46 | let actual_impl_str = actual_impl.to_string(); 47 | assert!( 48 | actual_impl_str.contains("unsafe impl :: ocaml_interop :: ToOCaml < MySimpleStruct >"), 49 | "Should generate ToOCaml impl" 50 | ); 51 | assert!( 52 | actual_impl_str.contains("field_a_ocaml"), 53 | "Should generate field_a_ocaml" 54 | ); 55 | assert!( 56 | actual_impl_str.contains("field_b_ocaml"), 57 | "Should generate field_b_ocaml" 58 | ); 59 | assert!( 60 | actual_impl_str.contains("field_c_ocaml"), 61 | "Should generate field_c_ocaml" 62 | ); 63 | } 64 | 65 | #[test] 66 | fn test_regular_enum_with_enums_replacing_boolean_flags() { 67 | let input_enum = quote! { 68 | enum MySimpleEnum { 69 | A, 70 | B(i32), 71 | C { val: String }, 72 | D, 73 | } 74 | }; 75 | 76 | let derive_input = syn::parse2(input_enum.clone()).unwrap(); 77 | let type_rep = parse_input(derive_input).unwrap(); 78 | 79 | // Check that we correctly parsed the enum 80 | match &type_rep.data { 81 | crate::common::parsing::TypeRepData::Enum { variants, kind, .. } => { 82 | assert_eq!(variants.len(), 4, "Should have 4 variants"); 83 | 84 | // Check that the enum kind is Regular (not polymorphic) 85 | assert_eq!(kind, &EnumKind::Regular); 86 | 87 | // Check variant A: Unit variant 88 | assert_eq!(variants[0].ident.to_string(), "A"); 89 | assert_eq!(variants[0].kind, VariantKind::Unit); 90 | 91 | // Check variant B: Tuple variant 92 | assert_eq!(variants[1].ident.to_string(), "B"); 93 | assert_eq!(variants[1].kind, VariantKind::Tuple); 94 | assert_eq!(variants[1].fields.len(), 1); 95 | 96 | // Check variant C: Struct variant 97 | assert_eq!(variants[2].ident.to_string(), "C"); 98 | assert_eq!(variants[2].kind, VariantKind::Struct); 99 | assert_eq!(variants[2].fields.len(), 1); 100 | assert_eq!( 101 | variants[2].fields[0].ident.as_ref().unwrap().to_string(), 102 | "val" 103 | ); 104 | 105 | // Check variant D: Unit variant 106 | assert_eq!(variants[3].ident.to_string(), "D"); 107 | assert_eq!(variants[3].kind, VariantKind::Unit); 108 | } 109 | _ => panic!("Expected enum type"), 110 | } 111 | 112 | // Now check that it expands correctly 113 | let actual_impl_result = expand_to_ocaml(input_enum); 114 | assert!( 115 | actual_impl_result.is_ok(), 116 | "expand_to_ocaml failed for simple enum: {:?}", 117 | actual_impl_result.err().map(|e| e.to_string()) 118 | ); 119 | 120 | // Extract the implementation 121 | let actual_impl = actual_impl_result.unwrap(); 122 | 123 | // Check that it contains the key parts we expect 124 | let actual_impl_str = actual_impl.to_string(); 125 | assert!( 126 | actual_impl_str.contains("unsafe impl :: ocaml_interop :: ToOCaml < MySimpleEnum >"), 127 | "Should generate ToOCaml impl" 128 | ); 129 | assert!( 130 | actual_impl_str.contains("MySimpleEnum :: A =>"), 131 | "Should handle A variant" 132 | ); 133 | assert!( 134 | actual_impl_str.contains("MySimpleEnum :: B ("), 135 | "Should handle B variant" 136 | ); 137 | assert!( 138 | actual_impl_str.contains("MySimpleEnum :: C"), 139 | "Should handle C variant" 140 | ); 141 | assert!( 142 | actual_impl_str.contains("MySimpleEnum :: D =>"), 143 | "Should handle D variant" 144 | ); 145 | } 146 | 147 | #[test] 148 | fn test_polymorphic_enum_with_enum_kind() { 149 | let input_enum = quote! { 150 | #[ocaml(polymorphic_variant)] 151 | enum MyPolyEnum { 152 | Unit, 153 | WithPayload(i32), 154 | } 155 | }; 156 | 157 | let derive_input = syn::parse2(input_enum.clone()).unwrap(); 158 | let type_rep = parse_input(derive_input).unwrap(); 159 | 160 | // Check that we correctly parsed the enum 161 | match &type_rep.data { 162 | crate::common::parsing::TypeRepData::Enum { variants, kind, .. } => { 163 | assert_eq!(variants.len(), 2, "Should have 2 variants"); 164 | 165 | // Check that the enum kind is Polymorphic 166 | assert_eq!(kind, &EnumKind::Polymorphic); 167 | 168 | // Check variant Unit: Unit variant 169 | assert_eq!(variants[0].ident.to_string(), "Unit"); 170 | assert_eq!(variants[0].kind, VariantKind::Unit); 171 | 172 | // Check variant WithPayload: Tuple variant 173 | assert_eq!(variants[1].ident.to_string(), "WithPayload"); 174 | assert_eq!(variants[1].kind, VariantKind::Tuple); 175 | assert_eq!(variants[1].fields.len(), 1); 176 | } 177 | _ => panic!("Expected enum type"), 178 | } 179 | 180 | // Now check that it expands correctly 181 | let actual_impl_result = expand_to_ocaml(input_enum); 182 | assert!( 183 | actual_impl_result.is_ok(), 184 | "expand_to_ocaml failed for polymorphic enum: {:?}", 185 | actual_impl_result.err().map(|e| e.to_string()) 186 | ); 187 | 188 | // Extract the implementation 189 | let actual_impl = actual_impl_result.unwrap(); 190 | 191 | // Check that it contains the key parts we expect 192 | let actual_impl_str = actual_impl.to_string(); 193 | assert!( 194 | actual_impl_str.contains("unsafe impl :: ocaml_interop :: ToOCaml < MyPolyEnum >"), 195 | "Should generate ToOCaml impl" 196 | ); 197 | assert!( 198 | actual_impl_str.contains("MyPolyEnum :: Unit =>"), 199 | "Should handle Unit variant" 200 | ); 201 | assert!( 202 | actual_impl_str.contains("MyPolyEnum :: WithPayload ("), 203 | "Should handle WithPayload variant" 204 | ); 205 | assert!( 206 | actual_impl_str.contains("caml_hash_variant"), 207 | "Should use caml_hash_variant for polymorphic variants" 208 | ); 209 | } 210 | -------------------------------------------------------------------------------- /derive/src/to_ocaml/mod.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | pub mod codegen; 5 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ocaml-interop: OCaml and Rust Integration 2 | 3 | _Zinc-iron alloy coating is used in parts that need very good corrosion protection._ 4 | 5 | **Important Notices:** 6 | * **API IS CONSIDERED UNSTABLE AT THE MOMENT AND IS LIKELY TO CHANGE IN THE FUTURE.** 7 | * **Starting with version `0.11.0`, only OCaml 5.x is supported.** 8 | 9 | This library facilitates interoperability between OCaml and Rust. 10 | 11 | This document provides a structured guide to using `ocaml-interop`, covering fundamental Foreign 12 | Function Interface (FFI) calls and progressing to more advanced concepts. 13 | 14 | ## Table of Contents 15 | 16 | - [Part 1: Initial Usage - A Brief Overview](user_guides::part1_initial_usage_a_brief_overview) 17 | - 1.1 Exporting Rust Functions to OCaml: An Introduction 18 | - 1.2 Invoking OCaml Functions from Rust: An Introduction 19 | - 1.3 The OCaml Runtime Handle: `OCamlRuntime` 20 | - [Part 2: Fundamental Concepts](user_guides::part2_fundamental_concepts) 21 | - 2.1 Representing OCaml Values within Rust 22 | - 2.2 Converting Data Between Rust and OCaml 23 | - [Part 3: Exporting Rust Functions to OCaml](user_guides::part3_exporting_rust_functions_to_ocaml) 24 | - 3.1 The `#[ocaml_interop::export]` Macro 25 | - 3.2 Argument Types and Rooting Considerations 26 | - 3.3 Return Types 27 | - 3.4 Panic Handling Mechanisms 28 | - 3.5 Bytecode Function Generation 29 | - 3.6 The `noalloc` Attribute 30 | - 3.7 Direct Primitive Type Mapping 31 | - [Part 4: Invoking OCaml Functions from Rust](user_guides::part4_invoking_ocaml_functions_from_rust) 32 | - 4.1 The `ocaml!` Macro 33 | - 4.2 Passing Arguments to OCaml 34 | - 4.3 Receiving Return Values from OCaml 35 | - 4.4 Handling OCaml Exceptions from Rust 36 | - [Part 5: Managing the OCaml Runtime (for Rust-driven programs)](user_guides::part5_managing_the_ocaml_runtime_for_rust_driven_programs) 37 | - 5.1 Runtime Initialization 38 | - 5.2 Acquiring the Domain Lock 39 | - [Part 6: Advanced Topics](user_guides::part6_advanced_topics) 40 | - 6.1 In-depth Examination of Lifetimes and GC Interaction 41 | - 6.2 `OCamlRef<'a, T>` Detailed Explanation 42 | - 6.3 Interacting with OCaml Closures 43 | - 6.4 Tuples 44 | - 6.5 Records 45 | - 6.6 Variants and Enums 46 | - 6.7 Polymorphic Variants 47 | - 6.8 Bigarrays (Placeholder) 48 | - 6.9 Threading Considerations (Placeholder) 49 | - [Part 7: Build and Link Instructions](user_guides::part7_build_and_link_instructions) 50 | - 7.1 OCaml Programs Calling Rust Code 51 | - 7.2 Rust Programs Calling OCaml Code 52 | 53 | ### References and Further Reading 54 | 55 | - OCaml Manual: [Chapter 20 Interfacing C with OCaml](https://v2.ocaml.org/manual/intfc.html) 56 | - [Safely Mixing OCaml and Rust](https://docs.google.com/viewer?a=v&pid=sites&srcid=ZGVmYXVsdGRvbWFpbnxtbHdvcmtzaG9wcGV8Z3g6NDNmNDlmNTcxMDk1YTRmNg) 57 | paper by Stephen Dolan. 58 | - [Safely Mixing OCaml and Rust](https://www.youtube.com/watch?v=UXfcENNM_ts) talk by Stephen Dolan. 59 | - [CAMLroot: revisiting the OCaml FFI](https://arxiv.org/abs/1812.04905). 60 | - [caml-oxide](https://github.com/stedolan/caml-oxide), the code from that paper. 61 | - [ocaml-rs](https://github.com/zshipko/ocaml-rs), another OCaml<->Rust FFI library. 62 | 63 | -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | # OCaml-Interop Examples 2 | 3 | This directory contains various examples demonstrating the use of the `ocaml-interop` library to call Rust code from OCaml and vice-versa. 4 | 5 | ## Running the Examples 6 | 7 | The method for running each example depends on whether it is primarily an OCaml program calling Rust, or a Rust program calling OCaml. 8 | 9 | ### OCaml-Driven Examples (using Dune) 10 | 11 | Many examples in this directory are OCaml programs that demonstrate calling Rust functions. These are typically integrated with Dune. To build and run these examples, navigate to the root of the `ocaml-interop` project and execute: 12 | 13 | ```bash 14 | opam exec -- dune test -f 15 | ``` 16 | This command will compile the necessary Rust libraries and OCaml executables, and then run the OCaml programs. Each such example usually resides in its own subdirectory (e.g., `tuples/`, `records/`) and contains both the Rust and OCaml source code, along with a `dune` file. 17 | 18 | ### Rust-Driven Examples (using Cargo) 19 | 20 | Some examples might be Rust programs that demonstrate calling OCaml functions. These examples are typically run using Cargo. To run these, you would usually navigate to the specific example's Rust project directory and execute: 21 | 22 | ```bash 23 | cargo test 24 | ``` 25 | Or a similar `cargo run` command if it's a binary. Refer to the specific `README.md` within such an example's directory for precise instructions if they differ. 26 | -------------------------------------------------------------------------------- /docs/examples/noalloc_primitives/README.md: -------------------------------------------------------------------------------- 1 | # Noalloc Primitives Example 2 | 3 | This example demonstrates the use of the `#[ocaml_interop::export(noalloc)]` attribute 4 | with primitive types that are passed without boxing/tagging between OCaml and Rust: 5 | - `f64` (OCaml `float`) 6 | - `i32` (OCaml `int32`) 7 | - `i64` (OCaml `int64`) 8 | - `isize` (OCaml `int`) 9 | - `bool` (OCaml `bool`) 10 | - `()` (OCaml `unit`) 11 | 12 | Functions marked with `noalloc` use an immutable reference to the OCaml runtime (`&OCamlRuntime`) 13 | and are expected not to allocate new OCaml values that would require garbage collection. 14 | -------------------------------------------------------------------------------- /docs/examples/noalloc_primitives/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names noalloc_primitives) 3 | (libraries noalloc_primitives_rust_lib threads.posix)) 4 | 5 | (rule 6 | (alias runtest) 7 | (action 8 | (run ./noalloc_primitives.exe))) 9 | -------------------------------------------------------------------------------- /docs/examples/noalloc_primitives/noalloc_primitives.ml: -------------------------------------------------------------------------------- 1 | (* OCaml-calls-Rust: noalloc_primitives example *) 2 | 3 | (* External declarations for Rust functions exported with noalloc *) 4 | external rust_process_float : float -> float = "" "rust_process_float" [@@noalloc] [@@unboxed] 5 | external rust_process_int64 : int64 -> int64 = "" "rust_process_int64" [@@noalloc] [@@unboxed] 6 | external rust_process_bool : (bool [@untagged]) -> (bool [@untagged]) = "" "rust_process_bool" [@@noalloc] 7 | external rust_process_unit_to_unit : (unit [@untagged]) -> (unit [@untagged]) = "" "rust_process_unit_to_unit" [@@noalloc] 8 | external rust_process_isize : (int [@untagged]) -> (int [@untagged]) = "" "rust_process_isize" [@@noalloc] 9 | external rust_process_i32 : int32 -> int32 = "" "rust_process_i32" [@@noalloc] [@@unboxed] 10 | 11 | external rust_combine_primitives_noalloc : (float [@unboxed]) -> (int64 [@unboxed]) -> (bool [@untagged]) -> (int64 [@unboxed]) = "" "rust_combine_primitives_noalloc" [@@noalloc] 12 | external rust_select_i64_noalloc : (float [@unboxed]) -> (int64 [@unboxed]) -> (bool [@untagged]) -> (int64 [@unboxed]) = "" "rust_select_i64_noalloc" [@@noalloc] 13 | external rust_log_primitives_noalloc : (float [@unboxed]) -> (int64 [@unboxed]) -> (bool [@untagged]) -> unit = "" "rust_log_primitives_noalloc" [@@noalloc] 14 | 15 | let () = 16 | print_endline "--- OCaml calling Rust (noalloc examples) ---"; flush stdout; 17 | 18 | (* Test rust_process_float *) 19 | let float_val = 3.14159 in 20 | let float_res = rust_process_float float_val in 21 | Printf.printf "OCaml: rust_process_float(%f) = %f\n%!" float_val float_res; 22 | assert (float_res = float_val *. 2.0); 23 | 24 | (* Test rust_process_int64 *) 25 | let i64_val = 1234567890123456789L in 26 | let i64_res = rust_process_int64 i64_val in 27 | Printf.printf "OCaml: rust_process_int64(%Ld) = %Ld\n%!" i64_val i64_res; 28 | assert (i64_res = Int64.add i64_val 100L); 29 | 30 | (* Test rust_process_bool *) 31 | let bool_val_true = true in 32 | let bool_res_false = rust_process_bool bool_val_true in 33 | Printf.printf "OCaml: rust_process_bool(%b) = %b\n%!" bool_val_true bool_res_false; 34 | assert (bool_res_false = not bool_val_true); 35 | let bool_val_false = false in 36 | let bool_res_true = rust_process_bool bool_val_false in 37 | Printf.printf "OCaml: rust_process_bool(%b) = %b\n%!" bool_val_false bool_res_true; 38 | assert (bool_res_true = not bool_val_false); 39 | 40 | (* Test rust_process_unit_to_unit *) 41 | print_endline "OCaml: calling rust_process_unit_to_unit..."; flush stdout; 42 | rust_process_unit_to_unit (); (* Implicitly asserts no exception *) 43 | print_endline "OCaml: rust_process_unit_to_unit returned."; flush stdout; 44 | 45 | (* Test rust_process_isize *) 46 | let isize_val = 42 in 47 | let isize_res = rust_process_isize isize_val in 48 | Printf.printf "OCaml: rust_process_isize(%d) = %d\n%!" isize_val isize_res; 49 | assert (isize_res = isize_val + 5); 50 | let isize_val_neg = -10 in 51 | let isize_res_neg = rust_process_isize isize_val_neg in 52 | Printf.printf "OCaml: rust_process_isize(%d) = %d\n%!" isize_val_neg isize_res_neg; 53 | assert (isize_res_neg = isize_val_neg + 5); 54 | 55 | (* Test rust_process_i32 *) 56 | let i32_val = 1073741823l in (* Max int32 / 2 roughly *) 57 | let i32_res = rust_process_i32 i32_val in 58 | Printf.printf "OCaml: rust_process_i32(%ldl) = %ldl\n%!" i32_val i32_res; 59 | assert (i32_res = Int32.mul i32_val 2l); 60 | let i32_val_neg = -536870912l in 61 | let i32_res_neg = rust_process_i32 i32_val_neg in 62 | Printf.printf "OCaml: rust_process_i32(%ldl) = %ldl\n%!" i32_val_neg i32_res_neg; 63 | assert (i32_res_neg = Int32.mul i32_val_neg 2l); 64 | 65 | (* Test rust_combine_primitives_noalloc *) 66 | let f_comb = 10.5 in 67 | let i_comb = 20L in 68 | let b_comb_true = true in 69 | let b_comb_false = false in 70 | let res_comb_true = rust_combine_primitives_noalloc f_comb i_comb b_comb_true in 71 | Printf.printf "OCaml: rust_combine_primitives_noalloc(%f, %Ld, %b) = %Ld\n%!" f_comb i_comb b_comb_true res_comb_true; 72 | assert (res_comb_true = Int64.add i_comb (Int64.of_float f_comb)); 73 | let res_comb_false = rust_combine_primitives_noalloc f_comb i_comb b_comb_false in 74 | Printf.printf "OCaml: rust_combine_primitives_noalloc(%f, %Ld, %b) = %Ld\n%!" f_comb i_comb b_comb_false res_comb_false; 75 | assert (res_comb_false = Int64.sub i_comb (Int64.of_float f_comb)); 76 | 77 | (* Test rust_select_i64_noalloc *) 78 | let f_sel = 5.5 in 79 | let i_sel = 100L in 80 | let b_sel_true = true in 81 | let b_sel_false = false in 82 | let res_sel_true = rust_select_i64_noalloc f_sel i_sel b_sel_true in 83 | Printf.printf "OCaml: rust_select_i64_noalloc(%f, %Ld, %b) = %Ld\n%!" f_sel i_sel b_sel_true res_sel_true; 84 | assert (res_sel_true = Int64.add i_sel (Int64.of_float f_sel)); 85 | let res_sel_false = rust_select_i64_noalloc f_sel i_sel b_sel_false in 86 | Printf.printf "OCaml: rust_select_i64_noalloc(%f, %Ld, %b) = %Ld\n%!" f_sel i_sel b_sel_false res_sel_false; 87 | assert (res_sel_false = Int64.sub i_sel (Int64.of_float f_sel)); 88 | 89 | (* Test rust_log_primitives_noalloc *) 90 | print_endline "OCaml: calling rust_log_primitives_noalloc(1.23, 987L, true)..."; flush stdout; 91 | rust_log_primitives_noalloc 1.23 987L true; (* Implicitly asserts no exception *) 92 | print_endline "OCaml: rust_log_primitives_noalloc returned."; flush stdout; 93 | 94 | print_endline "--- End of noalloc examples ---"; flush stdout; 95 | -------------------------------------------------------------------------------- /docs/examples/noalloc_primitives/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "noalloc_primitives_rust_lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["staticlib", "cdylib"] 9 | 10 | [dependencies] 11 | ocaml-interop = { path = "../../../../" } # Adjust path if example depth changes 12 | -------------------------------------------------------------------------------- /docs/examples/noalloc_primitives/rust/dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (targets libnoalloc_primitives_rust_lib.a dllnoalloc_primitives_rust_lib.so) 3 | (deps (source_tree src) dune-Cargo.toml) 4 | (action 5 | (no-infer 6 | (progn 7 | ;; macOS requires these flags because undefined symbols are not allowed by default 8 | (run sh -c " 9 | if [ \"$(uname -s)\" = \"Darwin\" ]; then 10 | export RUSTFLAGS='-C link-args=-Wl,-undefined,dynamic_lookup' 11 | fi 12 | mv dune-Cargo.toml Cargo.toml 13 | cargo build 14 | ") 15 | (run sh -c 16 | "cp target/debug/libnoalloc_primitives_rust_lib.so ./dllnoalloc_primitives_rust_lib.so 2> /dev/null || \ 17 | cp target/debug/libnoalloc_primitives_rust_lib.dylib ./dllnoalloc_primitives_rust_lib.so") 18 | (run cp target/debug/libnoalloc_primitives_rust_lib.a ./libnoalloc_primitives_rust_lib.a) 19 | )))) 20 | 21 | (library 22 | (name noalloc_primitives_rust_lib) 23 | (c_library_flags -lc -lm) 24 | (foreign_archives noalloc_primitives_rust_lib)) 25 | -------------------------------------------------------------------------------- /docs/examples/noalloc_primitives/rust/dune-Cargo.toml: -------------------------------------------------------------------------------- 1 | # To be used by `dune` where the relative path 2 | # to ocaml-interop is different and we don't 3 | # want to be inside the toplevel workspace. 4 | 5 | [package] 6 | name = "noalloc_primitives_rust_lib" 7 | version = "0.1.0" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["staticlib", "cdylib"] 12 | 13 | [dependencies] 14 | # Path relative to _build/default/docs/examples/noalloc_primitives/rust/ 15 | ocaml-interop = { path = "../../../../../../" } 16 | 17 | [workspace] 18 | -------------------------------------------------------------------------------- /docs/examples/noalloc_primitives/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use ocaml_interop::{OCaml, OCamlRuntime}; 5 | 6 | // Example with f64 (float) 7 | #[ocaml_interop::export(noalloc)] 8 | pub fn rust_process_float(_cr: &OCamlRuntime, input_float: f64) -> f64 { 9 | println!("[Rust] rust_process_float received: {}", input_float); 10 | input_float * 2.0 11 | } 12 | 13 | // Example with i64 (int64) 14 | #[ocaml_interop::export(noalloc)] 15 | pub fn rust_process_int64(_cr: &OCamlRuntime, input_i64: i64) -> i64 { 16 | println!("[Rust] rust_process_int64 received: {}", input_i64); 17 | input_i64 + 100 18 | } 19 | 20 | // Example with bool 21 | #[ocaml_interop::export(noalloc)] 22 | pub fn rust_process_bool(_cr: &OCamlRuntime, input_bool: bool) -> bool { 23 | println!("[Rust] rust_process_bool received: {}", input_bool); 24 | !input_bool 25 | } 26 | 27 | // Example with unit argument and unit return 28 | #[ocaml_interop::export(noalloc)] 29 | pub fn rust_process_unit_to_unit(_cr: &OCamlRuntime, _input_unit: OCaml<()>) { 30 | println!("[Rust] rust_process_unit_to_unit called"); 31 | // No explicit return needed for unit, it's implicit. 32 | } 33 | 34 | // Example with isize (OCaml int @untagged) 35 | #[ocaml_interop::export(noalloc)] 36 | pub fn rust_process_isize(_cr: &OCamlRuntime, input_isize: isize) -> isize { 37 | println!("[Rust] rust_process_isize received: {}", input_isize); 38 | input_isize + 5 39 | } 40 | 41 | // Example with i32 (OCaml int32) 42 | #[ocaml_interop::export(noalloc)] 43 | pub fn rust_process_i32(_cr: &OCamlRuntime, input_i32: i32) -> i32 { 44 | println!("[Rust] rust_process_i32 received: {}", input_i32); 45 | input_i32 * 2 46 | } 47 | 48 | #[ocaml_interop::export(noalloc)] 49 | pub fn rust_combine_primitives_noalloc(_cr: &OCamlRuntime, f: f64, i: i64, b: bool) -> i64 { 50 | println!( 51 | "[Rust] rust_combine_primitives_noalloc: f={}, i={}, b={}", 52 | f, i, b 53 | ); 54 | let mut result = i; 55 | if b { 56 | result += f as i64; 57 | } else { 58 | result -= f as i64; 59 | } 60 | result 61 | } 62 | 63 | // Example with multiple primitive arguments and returning one of them (noalloc friendly) 64 | #[ocaml_interop::export(noalloc)] 65 | pub fn rust_select_i64_noalloc(_cr: &OCamlRuntime, f: f64, i: i64, b: bool) -> i64 { 66 | println!("[Rust] rust_select_i64_noalloc: f={}, i={}, b={}", f, i, b); 67 | if b { 68 | i + (f as i64) 69 | } else { 70 | i - (f as i64) 71 | } 72 | } 73 | 74 | // Example returning unit, taking multiple primitives 75 | #[ocaml_interop::export(noalloc)] 76 | pub fn rust_log_primitives_noalloc(_cr: &OCamlRuntime, f: f64, i: i64, b: bool) { 77 | println!( 78 | "[Rust] rust_log_primitives_noalloc: float={}, int64={}, bool={}", 79 | f, i, b 80 | ); 81 | } 82 | -------------------------------------------------------------------------------- /docs/examples/polymorphic_variants/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names polymorphic_variants_example) 3 | (libraries polymorphic_variants_rust_lib threads.posix)) 4 | 5 | (rule 6 | (alias runtest) 7 | (action 8 | (run ./polymorphic_variants_example.exe))) 9 | -------------------------------------------------------------------------------- /docs/examples/polymorphic_variants/polymorphic_variants_example.ml: -------------------------------------------------------------------------------- 1 | type action = [ 2 | | `Start 3 | | `Stop 4 | | `Set_speed of int 5 | ] 6 | 7 | external rust_create_action : unit -> action = "rust_create_action" 8 | external rust_process_action : action -> string = "rust_process_action" 9 | 10 | let () = 11 | let created_action = rust_create_action () in 12 | let desc_created = 13 | match created_action with 14 | | `Start -> "`Start" 15 | | `Stop -> "`Stop" 16 | | `Set_speed n -> Printf.sprintf "`Set_speed %d" n 17 | in 18 | Printf.printf "[OCaml] Action from Rust creation: %s\n" desc_created; 19 | 20 | let processed_desc_created = rust_process_action created_action in 21 | Printf.printf "[OCaml] Action from Rust (processed): %s\n" processed_desc_created; 22 | 23 | let local_action : action = `Set_speed 75 in 24 | let processed_desc_local = rust_process_action local_action in 25 | Printf.printf "[OCaml] Local Action processed by Rust: %s\n" processed_desc_local 26 | -------------------------------------------------------------------------------- /docs/examples/polymorphic_variants/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "polymorphic_variants_rust_lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["staticlib", "cdylib"] 9 | 10 | [dependencies] 11 | ocaml-interop = { path = "../../../../" } 12 | -------------------------------------------------------------------------------- /docs/examples/polymorphic_variants/rust/dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (targets libpolymorphic_variants_rust_lib.a dllpolymorphic_variants_rust_lib.so) 3 | (deps (source_tree src) dune-Cargo.toml) 4 | (action 5 | (no-infer 6 | (progn 7 | ;; macOS requires these flags because undefined symbols are not allowed by default 8 | (run sh -c " 9 | if [ \"$(uname -s)\" = \"Darwin\" ]; then 10 | export RUSTFLAGS='-C link-args=-Wl,-undefined,dynamic_lookup' 11 | fi 12 | mv dune-Cargo.toml Cargo.toml 13 | cargo build 14 | ") 15 | (run sh -c 16 | "cp target/debug/libpolymorphic_variants_rust_lib.so ./dllpolymorphic_variants_rust_lib.so 2> /dev/null || \ 17 | cp target/debug/libpolymorphic_variants_rust_lib.dylib ./dllpolymorphic_variants_rust_lib.so") 18 | (run cp target/debug/libpolymorphic_variants_rust_lib.a ./libpolymorphic_variants_rust_lib.a) 19 | )))) 20 | 21 | (library 22 | (name polymorphic_variants_rust_lib) 23 | (c_library_flags -lc -lm) 24 | (foreign_archives polymorphic_variants_rust_lib)) 25 | -------------------------------------------------------------------------------- /docs/examples/polymorphic_variants/rust/dune-Cargo.toml: -------------------------------------------------------------------------------- 1 | # To be used by `dune` where the relative path 2 | # to ocaml-interop is different and we don't 3 | # want to be inside the toplevel workspace. 4 | 5 | [package] 6 | name = "polymorphic_variants_rust_lib" 7 | version = "0.1.0" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["staticlib", "cdylib"] 12 | 13 | [dependencies] 14 | # Path relative to _build/default/docs/examples/polymorphic_variants/rust/ 15 | ocaml-interop = { path = "../../../../../../" } 16 | 17 | [workspace] 18 | -------------------------------------------------------------------------------- /docs/examples/polymorphic_variants/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use ocaml_interop::{FromOCaml, OCaml, OCamlInt, OCamlRuntime, ToOCaml}; 5 | 6 | #[derive(Debug, Clone, FromOCaml, ToOCaml)] 7 | #[ocaml(polymorphic_variant)] 8 | pub enum Action { 9 | Start, 10 | Stop, 11 | #[ocaml(tag = "Set_speed")] 12 | SetSpeed(#[ocaml(as_ = "OCamlInt")] i64), 13 | } 14 | 15 | #[ocaml_interop::export] 16 | pub fn rust_create_action(cr: &mut OCamlRuntime, _unit: OCaml<()>) -> OCaml { 17 | let action_rust = Action::SetSpeed(100); 18 | action_rust.to_ocaml(cr) 19 | } 20 | 21 | #[ocaml_interop::export] 22 | pub fn rust_process_action(cr: &mut OCamlRuntime, action: OCaml) -> OCaml { 23 | let rust_action: Action = action.to_rust(); 24 | let description = match rust_action { 25 | Action::Start => "Rust processed: Start".to_string(), 26 | Action::Stop => "Rust processed: Stop".to_string(), // Added missing Stop case 27 | Action::SetSpeed(s) => format!("Rust processed: Set_speed to {}", s), 28 | }; 29 | description.to_ocaml(cr) 30 | } 31 | -------------------------------------------------------------------------------- /docs/examples/records/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names records_example) 3 | (libraries records_rust_lib threads.posix)) 4 | 5 | (rule 6 | (alias runtest) 7 | (action 8 | (run ./records_example.exe))) 9 | -------------------------------------------------------------------------------- /docs/examples/records/records_example.ml: -------------------------------------------------------------------------------- 1 | (* Define the OCaml record type. Field order must match Rust's struct declaration. *) 2 | type person = { 3 | full_name : string; 4 | birth_year : int; 5 | is_active : bool; 6 | } 7 | 8 | (* Declare the external Rust functions *) 9 | external rust_update_person_activity : person -> bool -> person = "rust_update_person_activity" 10 | external rust_create_person : string -> int -> bool -> person = "rust_create_person" 11 | 12 | let print_person_status p = 13 | Printf.printf "OCaml: Person Name: %s, Birth Year: %d, Active: %b\n%!" 14 | p.full_name p.birth_year p.is_active 15 | 16 | let () = 17 | let initial_person = { full_name = "John Doe"; birth_year = 1990; is_active = true } in 18 | Printf.printf "Initial person:\n%!"; 19 | print_person_status initial_person; 20 | 21 | let updated_person = rust_update_person_activity initial_person false in 22 | Printf.printf "\nUpdated person (activity changed to false by Rust):\n%!"; 23 | print_person_status updated_person; 24 | 25 | let new_person_from_rust = rust_create_person "Alice Wonderland" 2000 true in 26 | Printf.printf "\nNew person created by Rust:\n%!"; 27 | print_person_status new_person_from_rust; 28 | 29 | Printf.printf "\nTest complete.\n%!" 30 | -------------------------------------------------------------------------------- /docs/examples/records/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "records_rust_lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["staticlib", "cdylib"] 9 | 10 | [dependencies] 11 | ocaml-interop = { path = "../../../../" } 12 | -------------------------------------------------------------------------------- /docs/examples/records/rust/dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (targets librecords_rust_lib.a dllrecords_rust_lib.so) 3 | (deps (source_tree src) dune-Cargo.toml) 4 | (action 5 | (no-infer 6 | (progn 7 | ;; macOS requires these flags because undefined symbols are not allowed by default 8 | (run sh -c " 9 | if [ \"$(uname -s)\" = \"Darwin\" ]; then 10 | export RUSTFLAGS='-C link-args=-Wl,-undefined,dynamic_lookup' 11 | fi 12 | mv dune-Cargo.toml Cargo.toml 13 | cargo build 14 | ") 15 | (run sh -c 16 | "cp target/debug/librecords_rust_lib.so ./dllrecords_rust_lib.so 2> /dev/null || \\ 17 | cp target/debug/librecords_rust_lib.dylib ./dllrecords_rust_lib.so") 18 | (run cp target/debug/librecords_rust_lib.a ./librecords_rust_lib.a) 19 | )))) 20 | 21 | (library 22 | (name records_rust_lib) 23 | (foreign_archives records_rust_lib) 24 | (c_library_flags (-lc -lm))) 25 | -------------------------------------------------------------------------------- /docs/examples/records/rust/dune-Cargo.toml: -------------------------------------------------------------------------------- 1 | # To be used by `dune` where the relative path 2 | # to ocaml-interop is different and we don't 3 | # want to be inside the toplevel workspace. 4 | 5 | [package] 6 | name = "records_rust_lib" 7 | version = "0.1.0" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["staticlib", "cdylib"] 12 | 13 | [dependencies] 14 | ocaml-interop = { path = "../../../../../../" } # Path relative to _build/default/docs/examples/records/rust/ 15 | 16 | [workspace] 17 | -------------------------------------------------------------------------------- /docs/examples/records/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use ocaml_interop::{FromOCaml, OCaml, OCamlInt, OCamlRuntime, ToOCaml}; 5 | 6 | // Define the plain Rust struct. 7 | #[derive(Debug, Clone, ToOCaml, FromOCaml)] 8 | struct Person { 9 | full_name: String, 10 | #[ocaml(as_ = "OCamlInt")] 11 | birth_year: i64, 12 | is_active: bool, 13 | } 14 | 15 | #[ocaml_interop::export] 16 | pub fn rust_update_person_activity( 17 | cr: &mut OCamlRuntime, 18 | person_val: OCaml, // Input: OCaml person record, represented by OCaml 19 | new_activity_status: OCaml, 20 | ) -> OCaml { 21 | // Output: OCaml person record 22 | // Convert the incoming OCaml record to our Rust `Person` struct 23 | let mut person_rust: Person = person_val.to_rust(); 24 | 25 | // Modify the Rust struct 26 | person_rust.is_active = new_activity_status.to_rust(); 27 | person_rust.full_name = format!("{} (updated)", person_rust.full_name); 28 | 29 | // Convert the modified Rust struct back to an OCaml record for returning 30 | person_rust.to_ocaml(cr) 31 | } 32 | 33 | #[ocaml_interop::export] 34 | pub fn rust_create_person( 35 | cr: &mut OCamlRuntime, 36 | full_name_val: OCaml, 37 | birth_year_val: OCaml, 38 | is_active_val: OCaml, 39 | ) -> OCaml { 40 | // Create an instance of our Rust `Person` struct 41 | let person_rust = Person { 42 | full_name: full_name_val.to_rust(), 43 | birth_year: birth_year_val.to_rust(), 44 | is_active: is_active_val.to_rust(), 45 | }; 46 | // Convert the Rust struct to an OCaml record for returning 47 | person_rust.to_ocaml(cr) 48 | } 49 | -------------------------------------------------------------------------------- /docs/examples/tuples/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names tuples_example) 3 | (libraries alcotest base tuples_example_rust_lib threads.posix)) 4 | 5 | (rule 6 | (alias runtest) 7 | (action 8 | (run ./tuples_example.exe))) 9 | -------------------------------------------------------------------------------- /docs/examples/tuples/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tuples_example_rust_lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [lib] 7 | crate-type = ["staticlib"] 8 | 9 | [dependencies] 10 | ocaml-interop = { path = "../../../../" } 11 | -------------------------------------------------------------------------------- /docs/examples/tuples/rust/dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (targets libtuples_example_rust_lib.a dlltuples_example_rust_lib.so) 3 | (deps (source_tree src) dune-Cargo.toml) 4 | (action 5 | (no-infer 6 | (progn 7 | ;; macOS requires these flags because undefined symbols are not allowed by default 8 | (run sh -c " 9 | if [ \"$(uname -s)\" = \"Darwin\" ]; then 10 | export RUSTFLAGS='-C link-args=-Wl,-undefined,dynamic_lookup' 11 | fi 12 | mv dune-Cargo.toml Cargo.toml 13 | cargo build 14 | ") 15 | (run sh -c 16 | "cp target/debug/libtuples_example_rust_lib.so ./dlltuples_example_rust_lib.so 2> /dev/null || \ 17 | cp target/debug/libtuples_example_rust_lib.dylib ./dlltuples_example_rust_lib.so") 18 | (run cp target/debug/libtuples_example_rust_lib.a ./libtuples_example_rust_lib.a) 19 | )))) 20 | 21 | (library 22 | (name tuples_example_rust_lib) 23 | (c_library_flags -lc -lm) 24 | (foreign_archives tuples_example_rust_lib)) 25 | -------------------------------------------------------------------------------- /docs/examples/tuples/rust/dune-Cargo.toml: -------------------------------------------------------------------------------- 1 | # To be used by `dune` where the relative path 2 | # to ocaml-interop is different and we don't 3 | # want to be inside the toplevel workspace. 4 | 5 | [package] 6 | name = "tuples_example_rust_lib" 7 | version = "0.1.0" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["staticlib", "cdylib"] 12 | 13 | [dependencies] 14 | ocaml-interop = { path = "../../../../../.." } 15 | 16 | [workspace] -------------------------------------------------------------------------------- /docs/examples/tuples/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use ocaml_interop::{OCaml, OCamlInt, OCamlRuntime, ToOCaml}; 5 | 6 | #[ocaml_interop::export] 7 | pub fn process_ocaml_tuple( 8 | cr: &mut OCamlRuntime, 9 | input_tuple: OCaml<(OCamlInt, String)>, // OCaml type: int * string 10 | ) -> OCaml<(String, OCamlInt)> { 11 | // OCaml type: string * int 12 | 13 | // --- Individual Element Access (Demonstration) --- 14 | // You can access tuple elements individually using .fst() and .snd() 15 | // This returns OCaml references to the elements. 16 | let ocaml_int_ref = input_tuple.fst(); 17 | let ocaml_str_ref = input_tuple.snd(); 18 | 19 | let individual_rust_int: i64 = ocaml_int_ref.to_rust(); 20 | let individual_rust_string: String = ocaml_str_ref.to_rust(); 21 | println!( 22 | "[Rust] Individually accessed tuple elements: int = {}, string = \"{}\"", 23 | individual_rust_int, individual_rust_string 24 | ); 25 | 26 | // --- Full Tuple Conversion --- 27 | // For processing the entire tuple, convert it directly to a Rust tuple. 28 | // This is generally more concise if you need all elements. 29 | println!("[Rust] Converting the full OCaml tuple to a Rust tuple..."); 30 | let (rust_int, rust_string): (i64, String) = input_tuple.to_rust(); 31 | println!( 32 | "[Rust] Full Rust tuple: ({}, \"{}\")", 33 | rust_int, rust_string 34 | ); 35 | 36 | let processed_string = format!("Processed in Rust: {}", rust_string); 37 | let processed_int = rust_int + 100; 38 | 39 | println!( 40 | "[Rust] Processed Rust tuple: ({}, \"{}\")", 41 | processed_int, processed_string 42 | ); 43 | 44 | let result_tuple_rust = (processed_string, processed_int); 45 | 46 | println!("[Rust] Converting result to OCaml tuple and returning..."); 47 | result_tuple_rust.to_ocaml(cr) 48 | } 49 | -------------------------------------------------------------------------------- /docs/examples/tuples/tuples_example.ml: -------------------------------------------------------------------------------- 1 | (* This file demonstrates basic tuple interoperability between OCaml and Rust. *) 2 | 3 | (* External declaration for the Rust function `process_ocaml_tuple`. 4 | The type signature `int * string -> string * int` defines how OCaml 5 | interacts with the Rust function: it sends an (int, string) tuple 6 | and expects a (string, int) tuple in return. 7 | *) 8 | external process_ocaml_tuple : int * string -> string * int = "process_ocaml_tuple" 9 | 10 | let () = 11 | let my_ocaml_tuple = (42, "hello from OCaml world") in 12 | Printf.printf "OCaml: Sending tuple to Rust: (%d, \"%s\")\n%!" 13 | (fst my_ocaml_tuple) (snd my_ocaml_tuple); 14 | 15 | (* Call the Rust function. `ocaml-interop` handles the conversion 16 | of the OCaml tuple to a type Rust can understand, and vice-versa for the result. *) 17 | let (returned_string, returned_int) = process_ocaml_tuple my_ocaml_tuple in 18 | 19 | Printf.printf "OCaml: Received tuple from Rust: (\"%s\", %d)\n%!" 20 | returned_string returned_int; 21 | 22 | (* Expected output from both OCaml and Rust (via Rust's `println!`): 23 | OCaml: Sending tuple to Rust: (42, "hello from OCaml world") 24 | [Rust] Individually accessed tuple elements: int = 42, string = "hello from OCaml world" 25 | [Rust] Converting the full OCaml tuple to a Rust tuple... 26 | [Rust] Full Rust tuple: (42, "hello from OCaml world") 27 | [Rust] Processed Rust tuple: (142, "Processed in Rust: hello from OCaml world") 28 | [Rust] Converting result to OCaml tuple and returning... 29 | OCaml: Received tuple from Rust: ("Processed in Rust: hello from OCaml world", 142) 30 | *) 31 | () 32 | -------------------------------------------------------------------------------- /docs/examples/variants/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names variants_example) 3 | (libraries variants_rust_lib threads.posix)) 4 | 5 | (rule 6 | (alias runtest) 7 | (action 8 | (run ./variants_example.exe))) 9 | -------------------------------------------------------------------------------- /docs/examples/variants/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "variants_rust_lib" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [lib] 8 | crate-type = ["staticlib", "cdylib"] 9 | 10 | [dependencies] 11 | ocaml-interop = { path = "../../../../" } 12 | -------------------------------------------------------------------------------- /docs/examples/variants/rust/dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (targets libvariants_rust_lib.a dllvariants_rust_lib.so) 3 | (deps (source_tree src) dune-Cargo.toml) 4 | (action 5 | (no-infer 6 | (progn 7 | ;; macOS requires these flags because undefined symbols are not allowed by default 8 | (run sh -c " 9 | if [ \"$(uname -s)\" = \"Darwin\" ]; then 10 | export RUSTFLAGS='-C link-args=-Wl,-undefined,dynamic_lookup' 11 | fi 12 | mv dune-Cargo.toml Cargo.toml 13 | cargo build 14 | ") 15 | (run sh -c 16 | "cp target/debug/libvariants_rust_lib.so ./dllvariants_rust_lib.so 2> /dev/null || \ 17 | cp target/debug/libvariants_rust_lib.dylib ./dllvariants_rust_lib.so") 18 | (run cp target/debug/libvariants_rust_lib.a ./libvariants_rust_lib.a) 19 | )))) 20 | 21 | (library 22 | (name variants_rust_lib) 23 | (c_library_flags -lc -lm) 24 | (foreign_archives variants_rust_lib)) 25 | -------------------------------------------------------------------------------- /docs/examples/variants/rust/dune-Cargo.toml: -------------------------------------------------------------------------------- 1 | # To be used by `dune` where the relative path 2 | # to ocaml-interop is different and we don't 3 | # want to be inside the toplevel workspace. 4 | 5 | [package] 6 | name = "variants_rust_lib" 7 | version = "0.1.0" 8 | edition = "2021" 9 | 10 | [lib] 11 | crate-type = ["staticlib", "cdylib"] 12 | 13 | [dependencies] 14 | # Path relative to _build/default/docs/examples/variants/rust/ 15 | ocaml-interop = { path = "../../../../../../" } 16 | 17 | [workspace] 18 | -------------------------------------------------------------------------------- /docs/examples/variants/rust/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use ocaml_interop::{FromOCaml, OCaml, OCamlInt, OCamlRuntime, ToOCaml}; 5 | 6 | // Rust enum mirroring the OCaml variant type. 7 | // Order of variants must match OCaml definition. 8 | #[derive(Debug, PartialEq, Clone, FromOCaml, ToOCaml)] 9 | #[ocaml(as_ = "OCamlStatus")] 10 | pub enum Status { 11 | Ok, 12 | Error(String), 13 | Retrying(#[ocaml(as_ = "OCamlInt")] i64), 14 | } 15 | 16 | // Rust marker type for the OCaml `status` variant. 17 | // This is used in type signatures like OCaml. 18 | pub enum OCamlStatus {} 19 | 20 | // Exported Rust function that takes an OCaml `status`, 21 | // converts it to Rust `Status`, processes it, and returns a string. 22 | #[ocaml_interop::export] 23 | pub fn rust_process_status(cr: &mut OCamlRuntime, status_val: OCaml) -> OCaml { 24 | let status_rust: Status = status_val.to_rust(); 25 | let result_string = match status_rust { 26 | Status::Ok => "Rust received: Ok".to_string(), 27 | Status::Error(s) => format!("Rust received: Error(\"{}\")", s), 28 | Status::Retrying(n) => format!("Rust received: Retrying({})", n), 29 | }; 30 | result_string.to_ocaml(cr) 31 | } 32 | 33 | // Exported Rust function that creates and returns an OCaml `Ok` variant. 34 | #[ocaml_interop::export] 35 | pub fn rust_create_status_ok(cr: &mut OCamlRuntime, _unused: OCaml<()>) -> OCaml { 36 | Status::Ok.to_ocaml(cr) 37 | } 38 | 39 | // Exported Rust function that creates and returns an OCaml `Error` variant. 40 | #[ocaml_interop::export] 41 | pub fn rust_create_status_error( 42 | cr: &mut OCamlRuntime, 43 | message: OCaml, 44 | ) -> OCaml { 45 | Status::Error(message.to_rust()).to_ocaml(cr) 46 | } 47 | 48 | // Exported Rust function that creates and returns an OCaml `Retrying` variant. 49 | #[ocaml_interop::export] 50 | pub fn rust_create_status_retrying( 51 | cr: &mut OCamlRuntime, 52 | count: OCaml, 53 | ) -> OCaml { 54 | Status::Retrying(count.to_rust()).to_ocaml(cr) 55 | } 56 | -------------------------------------------------------------------------------- /docs/examples/variants/variants_example.ml: -------------------------------------------------------------------------------- 1 | (* OCaml variant type definition. *) 2 | (* The order of constructors must match the order in Rust's enum declaration. *) 3 | type status = 4 | | Ok (* First constructor, no arguments *) 5 | | Error of string (* Second constructor, one argument *) 6 | | Retrying of int (* Third constructor, one argument *) 7 | 8 | (* External declarations for Rust functions. *) 9 | external rust_process_status : status -> string = "rust_process_status" 10 | external rust_create_status_ok : unit -> status = "rust_create_status_ok" 11 | external rust_create_status_error : string -> status = "rust_create_status_error" 12 | external rust_create_status_retrying : int -> status = "rust_create_status_retrying" 13 | 14 | (* Helper function to print OCaml status values. *) 15 | let print_status_details (s: status) (source: string) = 16 | match s with 17 | | Ok -> Printf.printf "%s: Status is Ok\n%!" source 18 | | Error msg -> Printf.printf "%s: Status is Error(\"%s\")\n%!" source msg 19 | | Retrying count -> Printf.printf "%s: Status is Retrying(%d)\n%!" source count 20 | 21 | let () = 22 | Printf.printf "--- Testing OCaml -> Rust status processing ---\n%!"; 23 | let status_ocaml_ok = Ok in 24 | let status_ocaml_error = Error "An OCaml-side problem" in 25 | let status_ocaml_retrying = Retrying 3 in 26 | 27 | print_status_details status_ocaml_ok "OCaml (original)"; 28 | let result_ok_processed = rust_process_status status_ocaml_ok in 29 | Printf.printf "Rust processed to: %s\n%!" result_ok_processed; 30 | 31 | print_status_details status_ocaml_error "OCaml (original)"; 32 | let result_error_processed = rust_process_status status_ocaml_error in 33 | Printf.printf "Rust processed to: %s\n%!" result_error_processed; 34 | 35 | print_status_details status_ocaml_retrying "OCaml (original)"; 36 | let result_retrying_processed = rust_process_status status_ocaml_retrying in 37 | Printf.printf "Rust processed to: %s\n%!" result_retrying_processed; 38 | 39 | Printf.printf "--- Testing Rust -> OCaml status creation ---\n%!"; 40 | 41 | let status_rust_ok = rust_create_status_ok () in 42 | print_status_details status_rust_ok "OCaml (from Rust)"; 43 | 44 | let status_rust_error = rust_create_status_error "A Rust-side problem" in 45 | print_status_details status_rust_error "OCaml (from Rust)"; 46 | 47 | let status_rust_retrying = rust_create_status_retrying 7 in 48 | print_status_details status_rust_retrying "OCaml (from Rust)"; 49 | 50 | Printf.printf "\nariants example test complete.\n%!" 51 | -------------------------------------------------------------------------------- /docs/part1-initial-usage-a-brief-overview.md: -------------------------------------------------------------------------------- 1 | ## Part 1: Initial Usage - A Brief Overview 2 | 3 | This section introduces basic examples to demonstrate the core functionality of [`ocaml-interop`]. 4 | 5 | ### 1.1 Exporting Rust Functions to OCaml: An Introduction 6 | 7 | Rust functions can be exposed to OCaml utilizing the [`#[ocaml_interop::export]`](export) procedural macro. 8 | 9 | **Rust (`src/lib.rs` or designated Rust library):** 10 | ```rust 11 | use ocaml_interop::{OCaml, OCamlInt, OCamlRuntime, ToOCaml}; 12 | 13 | #[ocaml_interop::export] 14 | pub fn rust_add_one(cr: &mut OCamlRuntime, num: OCaml) -> OCaml { 15 | let rust_num: i64 = num.to_rust(); 16 | let result = rust_num + 1; 17 | result.to_ocaml(cr) 18 | } 19 | ``` 20 | 21 | **OCaml (e.g., `main.ml`):** 22 | ```ocaml 23 | (* Declare the external Rust function *) 24 | external rust_add_one : int -> int = "rust_add_one" 25 | 26 | let () = 27 | let five = rust_add_one 4 in 28 | Printf.printf "4 + 1 = %d\n" five (* Output: 4 + 1 = 5 *) 29 | ``` 30 | The [`#[ocaml_interop::export]`](export) macro manages FFI boilerplate and panic safety mechanisms. It exposes 31 | OCaml values to Rust in a type-safe manner; subsequent conversion to Rust types must be performed 32 | explicitly by the developer. These aspects will be detailed subsequently. 33 | 34 | ### 1.2 Invoking OCaml Functions from Rust: An Introduction 35 | 36 | To call OCaml functions from Rust, the [`ocaml!`] macro is typically employed subsequent to the 37 | registration of the OCaml function. 38 | 39 | **OCaml (e.g., `my_ocaml_lib.ml`):** 40 | ```ocaml 41 | let multiply_by_two x = x * 2 42 | 43 | let () = 44 | Callback.register "multiply_by_two" multiply_by_two 45 | ``` 46 | This OCaml code must be compiled and linked with the Rust program. 47 | 48 | **Rust (`main.rs`):** 49 | ```rust,no_run 50 | use ocaml_interop::{ 51 | OCaml, OCamlInt, OCamlRuntime, OCamlRuntimeStartupGuard, ToOCaml, BoxRoot 52 | }; 53 | 54 | // Declare the OCaml function signature 55 | mod ocaml_bindings { 56 | use ocaml_interop::{ocaml, OCamlInt}; 57 | 58 | ocaml! { 59 | pub fn multiply_by_two(num: OCamlInt) -> OCamlInt; 60 | } 61 | } 62 | 63 | fn main() -> Result<(), String> { 64 | // Initialize the OCaml runtime if Rust is the primary execution context 65 | let _guard: OCamlRuntimeStartupGuard = OCamlRuntime::init()?; 66 | 67 | OCamlRuntime::with_domain_lock(|cr| { 68 | let rust_val: i64 = 10; 69 | 70 | // Pass direct Rust values - they're automatically converted 71 | let result_root: BoxRoot = ocaml_bindings::multiply_by_two(cr, rust_val); 72 | let rust_result: i64 = result_root.to_rust(cr); 73 | println!("10 * 2 = {}", rust_result); // Output: 10 * 2 = 20 74 | 75 | // Alternative: Traditional approach with explicit rooting still works 76 | let ocaml_val: BoxRoot = rust_val.to_boxroot(cr); 77 | let result_root: BoxRoot = ocaml_bindings::multiply_by_two(cr, &ocaml_val); 78 | let rust_result: i64 = result_root.to_rust(cr); 79 | println!("10 * 2 = {}", rust_result); // Output: 10 * 2 = 20 80 | }); 81 | Ok(()) 82 | } 83 | ``` 84 | 85 | ### 1.3 The OCaml Runtime Handle: [`OCamlRuntime`] 86 | 87 | Interactions with the OCaml runtime require access to an [`OCamlRuntime`] instance, 88 | conventionally named `cr`. 89 | - A mutable reference, [`&mut OCamlRuntime`](OCamlRuntime), is necessary for operations that may allocate OCaml 90 | values or trigger the OCaml Garbage Collector (GC). This is the most common requirement. 91 | - An immutable reference, [`&OCamlRuntime`](OCamlRuntime), can be sufficient for operations that only read from 92 | the OCaml heap without modifying it or causing allocations (e.g., some conversion methods on 93 | already existing [`OCaml`](OCaml) values). 94 | - When a Rust function is exported using [`#[ocaml_interop::export]`](export), [`cr: &mut OCamlRuntime`](OCamlRuntime) is 95 | automatically supplied as the initial argument. 96 | - When invoking OCaml from a Rust-driven program, `cr` (typically [`&mut OCamlRuntime`](OCamlRuntime)) is 97 | obtained via [`OCamlRuntime::with_domain_lock(|cr| { /* ... */ })`](OCamlRuntime::with_domain_lock) following runtime 98 | initialization. 99 | 100 | This handle is indispensable for managing OCaml's state, memory, and domain locks. 101 | -------------------------------------------------------------------------------- /docs/part2-fundamental-concepts.md: -------------------------------------------------------------------------------- 1 | ## Part 2: Fundamental Concepts 2 | 3 | A thorough understanding of these core types is essential for the effective utilization of [`ocaml-interop`]. 4 | 5 | ### 2.1 Representing OCaml Values within Rust 6 | 7 | - **[`OCaml<'gc, T>`](OCaml):** This type serves as the primary wrapper for an OCaml value within Rust code. 8 | - `'gc`: Represents a lifetime parameter associated with an active OCaml runtime scope. This 9 | signifies that the OCaml Garbage Collector (GC) may relocate or deallocate the value if it 10 | is not "rooted." 11 | - `T`: Denotes the Rust type corresponding to the OCaml value (e.g., [`OCamlInt`], `String`, 12 | [`OCamlList`](OCamlList)). 13 | - Instances of this type are generally ephemeral and should be regarded as potentially invalid 14 | subsequent to any invocation into the OCaml runtime, unless explicitly rooted. 15 | 16 | - **[`BoxRoot`](BoxRoot):** A smart pointer that "roots" an OCaml value, thereby ensuring that the OCaml 17 | GC does not deallocate or move it while the [`BoxRoot`](BoxRoot) instance persists. 18 | - It adheres to the RAII (Resource Acquisition Is Initialization) principle: the OCaml value 19 | is automatically unrooted when the [`BoxRoot`](BoxRoot) instance is dropped. 20 | - This mechanism is crucial for safely retaining OCaml values across multiple operations or 21 | Rust scopes. 22 | - Instances are created by: 23 | - Calling [`.to_boxroot(cr)`](ToOCaml::to_boxroot) on a Rust value (e.g., `rust_value.to_boxroot(cr)`), which 24 | converts it to an OCaml value and roots it. 25 | - Calling [`.root()`](OCaml::root) on an existing [`OCaml`](OCaml) value (e.g., `ocaml_val.root()`). 26 | - Using [`BoxRoot::new(ocaml_val)`](BoxRoot::new) with an existing [`OCaml`](OCaml) value. Note that 27 | `BoxRoot::new()` will panic if the underlying boxroot allocation fails. 28 | - [`BoxRoot`](BoxRoot) is `!Send` and `!Sync` due to its direct interaction with OCaml's 29 | domain-specific GC state and memory management, meaning it cannot be safely transferred 30 | across threads or shared concurrently between threads. 31 | 32 | - **[`RawOCaml`]:** An unsafe, raw pointer-sized type representing an OCaml value. Direct 33 | interaction with this type is infrequent, as [`OCaml`](OCaml) and [`BoxRoot`](BoxRoot) provide safe 34 | abstractions. 35 | 36 | ### 2.2 Converting Data Between Rust and OCaml 37 | 38 | The traits [`ToOCaml`](ToOCaml) and [`FromOCaml`](FromOCaml) are provided to facilitate data conversions. 39 | 40 | - **[`ToOCaml`](ToOCaml):** 41 | - Implemented by Rust types that are convertible to OCaml values of `OCamlType`. 42 | - Provides the method [`.to_ocaml(cr: &mut OCamlRuntime)`](ToOCaml::to_ocaml) to create an 43 | OCaml value. 44 | - Provides the method [`.to_boxroot(cr: &mut OCamlRuntime)`](ToOCaml::to_boxroot) to create 45 | a rooted OCaml value. 46 | - Example: `let ocaml_string: OCaml = "hello".to_ocaml(cr);`; 47 | 48 | - **[`FromOCaml`](FromOCaml):** 49 | - Implemented by Rust types that can be instantiated from OCaml values of `OCamlType`. 50 | - [`OCaml`](OCaml) provides [`.to_rust::()`](OCaml::to_rust). 51 | - [`BoxRoot`](BoxRoot) provides `.to_rust::(cr: &mut OCamlRuntime)`. 52 | - Example: `let rust_int: i64 = ocaml_int_value.to_rust();` 53 | 54 | **Common Type Mappings:** 55 | - Rust `i64` corresponds to OCaml `int` (represented as [`OCamlInt`]). 56 | - Rust `f64` corresponds to OCaml `float` (represented as [`OCamlFloat`]). 57 | - Rust `String`/`&str` corresponds to OCaml `string`. 58 | - Rust `Vec` corresponds to OCaml `list` or `array` (e.g., [`OCamlList`](OCamlList), 59 | [`OCamlUniformArray`](OCamlUniformArray)). 60 | - Rust `Option` corresponds to OCaml `option`. 61 | - Rust `Result` corresponds to OCaml `result` (often with [`OCaml`](OCaml) for error types). 62 | -------------------------------------------------------------------------------- /docs/part4-invoking-ocaml-functions-from-rust.md: -------------------------------------------------------------------------------- 1 | ## Part 4: Invoking OCaml Functions from Rust 2 | 3 | The [`ocaml!`] macro is used to declare Rust bindings for OCaml functions. 4 | 5 | ### 4.1 The [`ocaml!`] Macro 6 | 7 | This macro defines Rust function bindings for OCaml functions that have been registered on the 8 | OCaml side using `Callback.register "ocaml_function_name" ocaml_function_name`. 9 | 10 | **Example:** 11 | 12 | Suppose you have the following OCaml code: 13 | ```ocaml 14 | (* In your OCaml library, e.g., my_ocaml_lib.ml *) 15 | let greet name = Printf.printf "Hello, %s!\n" name 16 | let add_ints a b = a + b 17 | 18 | let () = 19 | Callback.register "ml_greet" greet; 20 | Callback.register "ml_add_ints" add_ints 21 | ``` 22 | 23 | To call these from Rust, you would declare them using the [`ocaml!`] macro: 24 | 25 | ```rust 26 | mod ocaml_api { 27 | use ocaml_interop::{ocaml, OCamlInt}; 28 | 29 | ocaml! { 30 | // OCaml: val ml_greet : string -> unit 31 | // Effective Rust signature generated: 32 | // pub fn ml_greet(cr: &mut OCamlRuntime, name: OCamlRef); 33 | pub fn ml_greet(name: String); 34 | 35 | // OCaml: val ml_add_ints : int -> int -> int 36 | // Effective Rust signature generated: 37 | // pub fn ml_add_ints(cr: &mut OCamlRuntime, a: OCamlRef, b: OCamlRef) -> BoxRoot; 38 | pub fn ml_add_ints(a: OCamlInt, b: OCamlInt) -> OCamlInt; 39 | } 40 | } 41 | ``` 42 | 43 | Key points about the [`ocaml!`] macro usage: 44 | - The macro generates Rust functions (e.g., `ocaml_api::ml_greet(cr, ...)` and 45 | `ocaml_api::ml_add_ints(cr, ...)`). 46 | - The first argument to these generated Rust functions is automatically [`cr: &mut OCamlRuntime`](OCamlRuntime), 47 | even if not explicitly listed in the `ocaml!{}` block for a given function signature. 48 | - **Flexible Parameters**: Arguments accept both [`OCamlRef`](OCamlRef) references and direct Rust values 49 | convertible to OCaml. You can pass either `&boxroot_value` or `direct_rust_value`. 50 | - Return types specified in `ocaml!{}` (e.g., `-> OCamlInt`) indicate the type contained 51 | within the [`BoxRoot`](BoxRoot) that the generated Rust function will return. 52 | 53 | ### 4.2 Passing Arguments to OCaml 54 | 55 | - **Flexible Parameters**: Functions generated by [`ocaml!`] can accept either direct Rust values 56 | (like `42`, `"hello"`, `vec![1,2,3]`) or [`OCamlRef<'a, T>`](OCamlRef) references (like `&boxroot`). 57 | - When passing direct Rust values, they are automatically converted to OCaml using [`ToOCaml`]. 58 | - When passing [`OCamlRef<'a, T>`](OCamlRef) references, they are used directly without conversion. 59 | - [`OCamlRef<'a, T>`](OCamlRef) can be obtained from [`OCaml<'gc, T>`](OCaml) or [`BoxRoot`](BoxRoot) using `&`. 60 | 61 | #### 4.2.1 Using Direct Rust Values (Recommended for Simple Cases) 62 | 63 | ```rust 64 | # use ocaml_interop::{OCamlRuntime, ocaml, OCamlInt, OCamlRuntimeStartupGuard}; 65 | # mod ocaml_api { 66 | # use ocaml_interop::{ocaml, OCamlInt}; 67 | # ocaml! { pub fn add_ints(a: OCamlInt, b: OCamlInt) -> OCamlInt; } 68 | # ocaml! { pub fn greet(name: String); } 69 | # } 70 | # fn test() -> Result<(), String> { 71 | # let _guard: OCamlRuntimeStartupGuard = OCamlRuntime::init()?; 72 | OCamlRuntime::with_domain_lock(|cr| { 73 | // Pass direct Rust values - they're automatically converted 74 | let result = ocaml_api::add_ints(cr, 42, 37); 75 | let sum: i64 = result.to_rust(cr); 76 | 77 | // Works with strings too 78 | ocaml_api::greet(cr, "World"); 79 | 80 | // Or string references 81 | let name = "Alice".to_string(); 82 | ocaml_api::greet(cr, &name); 83 | }); 84 | # Ok(()) 85 | # } 86 | ``` 87 | 88 | #### 4.2.2 Using OCamlRef Arguments (For Complex Scenarios) 89 | 90 | - **Role of the Borrow Checker**: `ocaml-interop` leverages Rust's borrow checker. An 91 | [`OCaml<'gc, T>`](OCaml) value is tied to the lifetime (`'gc`) of the [`OCamlRuntime`] reference (`cr`) 92 | from which it was obtained or with which it is associated. If an operation occurs that 93 | mutably borrows `cr` (e.g., another call into OCaml, allocation), the borrow checker will 94 | prevent the use of pre-existing [`OCaml<'gc, T>`](OCaml) values that might have been invalidated by 95 | potential GC activity. This is a key safety feature. 96 | - **When to Use [`BoxRoot`](BoxRoot) for Arguments (and the Borrow Checker)**: 97 | The primary reason for explicitly using [`BoxRoot`](BoxRoot) for arguments when calling OCaml functions 98 | (defined via [`ocaml!`]) stems from Rust's borrow checking rules interacting with the 99 | [`&mut OCamlRuntime`](OCamlRuntime) requirement of these functions. 100 | 101 | 1. **The [`&mut OCamlRuntime`](OCamlRuntime) Conflict**: Functions generated by [`ocaml!`] take `cr: &mut OCamlRuntime` 102 | as their first, implicit argument. If you create a temporary, unrooted OCaml value like 103 | `let ocaml_arg = rust_data.to_ocaml(cr);`, this `ocaml_arg` (of type [`OCaml<'gc, T>`](OCaml)) 104 | holds an immutable borrow on `cr` for its lifetime `'gc`. When you then attempt to call 105 | an OCaml function, e.g., `ocaml_api::some_func(cr, &ocaml_arg)`, a borrow conflict arises: 106 | `some_func` requires a mutable borrow of `cr`, but `ocaml_arg` still immutably borrows it. 107 | Rust's borrow checker will prevent this. 108 | 109 | 2. **`BoxRoot` as the Workaround**: Converting the Rust data to a [`BoxRoot`](BoxRoot) using 110 | `let ocaml_arg_rooted = rust_data.to_boxroot(cr);` resolves this. A [`BoxRoot`](BoxRoot) 111 | registers the OCaml value with the GC independently. Its validity is not tied to the 112 | specific borrow of `cr` used for its creation in the same way an [`OCaml<'gc, T>`](OCaml) is. 113 | Thus, you can pass [`&mut OCamlRuntime`](OCamlRuntime) to the OCaml function and pass `&ocaml_arg_rooted` 114 | (which becomes an [`OCamlRef`](OCamlRef)) without a borrow checker conflict. 115 | 116 | 3. **In Practice**: This pattern is mainly needed when you need to store OCaml values longer-term 117 | or when the borrow checker prevents using direct values. For simple cases, direct Rust values 118 | are easier and recommended. 119 | 120 | ```rust 121 | # use ocaml_interop::{OCamlRuntime, ToOCaml, BoxRoot, OCamlRef, OCamlFloat, OCamlRuntimeStartupGuard}; 122 | # mod ocaml_api { 123 | # use ocaml_interop::{ocaml, OCamlFloat, OCamlRuntime, BoxRoot, OCamlRef}; 124 | # ocaml! { pub fn add_floats(a: OCamlFloat, b: OCamlFloat) -> OCamlFloat; } 125 | # } 126 | # fn test() -> Result<(), String> { 127 | # let _guard: OCamlRuntimeStartupGuard = OCamlRuntime::init()?; 128 | OCamlRuntime::with_domain_lock(|cr| { 129 | let val_a_rust: f64 = 5.5; 130 | let val_b_rust: f64 = 2.3; 131 | 132 | // Option 1: Use direct values (recommended for simple cases) 133 | let sum_ocaml: BoxRoot = ocaml_api::add_floats(cr, val_a_rust, val_b_rust); 134 | let sum_rust: f64 = sum_ocaml.to_rust(cr); 135 | println!("Sum using direct values: {sum_rust}"); 136 | 137 | // Option 2: Use BoxRoot when needed for complex scenarios 138 | let val_a_ocaml: BoxRoot = val_a_rust.to_boxroot(cr); 139 | let val_b_ocaml: BoxRoot = val_b_rust.to_boxroot(cr); 140 | let sum_ocaml: BoxRoot = ocaml_api::add_floats(cr, &val_a_ocaml, &val_b_ocaml); 141 | let sum_rust: f64 = sum_ocaml.to_rust(cr); 142 | println!("Sum using BoxRoot: {sum_rust}"); 143 | }); 144 | # Ok(()) 145 | # } 146 | ``` 147 | 148 | ### 4.3 Receiving Return Values from OCaml 149 | 150 | - Functions declared using [`ocaml!`] that return a value will yield a [`BoxRoot`](BoxRoot). 151 | - This ensures that the returned OCaml value is immediately rooted and thus safe for use 152 | within Rust. 153 | - It can subsequently be converted to a Rust type using `.to_rust(cr)`. 154 | 155 | ### 4.4 Handling OCaml Exceptions from Rust 156 | 157 | - If an OCaml function invoked from Rust raises an exception, this will currently manifest as 158 | a **Rust panic**. 159 | - **Recommendation:** It is advisable to design OCaml functions intended for FFI with Rust to 160 | signal error conditions by returning `option` or `result` types (e.g., `int option`, 161 | `(string, string) result`), rather than by raising exceptions. These can then be mapped to 162 | Rust’s `Option` and `Result` types. 163 | -------------------------------------------------------------------------------- /docs/part5-managing-the-ocaml-runtime-for-rust-driven-programs.md: -------------------------------------------------------------------------------- 1 | ## Part 5: Managing the OCaml Runtime (for Rust-driven programs) 2 | 3 | If a program originates in Rust and requires interaction with OCaml code, the OCaml runtime must 4 | be explicitly managed by the Rust code. 5 | **Conversely, if OCaml invokes Rust code as a library, the OCaml runtime will have already been 6 | initialized; [`OCamlRuntime::init()`](OCamlRuntime::init) must not be called in such scenarios.** 7 | 8 | ### 5.1 Runtime Initialization 9 | 10 | - **[`OCamlRuntime::init`]`() -> Result<`[`OCamlRuntimeStartupGuard`]`, String>`:** 11 | - Initializes the OCaml C runtime and the `boxroot` system for GC interoperability. 12 | - This function should be called once at the startup of the Rust program. 13 | - **[`OCamlRuntimeStartupGuard`]:** 14 | - An RAII guard instance returned by [`OCamlRuntime::init()`](OCamlRuntime::init). 15 | - Upon being dropped, this guard automatically executes 16 | `boxroot_teardown` and `caml_shutdown` to ensure proper cleanup of the OCaml runtime. 17 | - It is `!Send` and `!Sync`. 18 | 19 | ```rust,no_run 20 | use ocaml_interop::{OCamlRuntime, OCamlRuntimeStartupGuard}; 21 | 22 | fn main() -> Result<(), String> { 23 | let _guard: OCamlRuntimeStartupGuard = OCamlRuntime::init()?; 24 | // The OCaml runtime is now active. 25 | // ... OCaml operations are performed here ... 26 | Ok(()) 27 | } // _guard is dropped at this point, thereby shutting down the OCaml runtime. 28 | ``` 29 | 30 | ### 5.2 Acquiring the Domain Lock 31 | 32 | Most OCaml operations mandate that the current thread holds the OCaml domain lock. 33 | - **[`OCamlRuntime::with_domain_lock`]`(|cr: &mut OCamlRuntime| { /* ... */ })`:** 34 | - This is the canonical method for obtaining the [`&mut OCamlRuntime`](OCamlRuntime) handle (`cr`). 35 | - It ensures that the current thread is registered as an OCaml domain and acquires the OCaml 36 | runtime lock. The lock is released upon completion of the closure. 37 | - All OCaml interactions should typically be performed within this closure. 38 | 39 | ```rust,no_run 40 | # use ocaml_interop::{OCamlRuntime, OCamlRuntimeStartupGuard, ToOCaml, OCaml}; 41 | # fn main() -> Result<(), String> { 42 | # let _guard = OCamlRuntime::init()?; 43 | OCamlRuntime::with_domain_lock(|cr| { 44 | // 'cr' is the &mut OCamlRuntime. 45 | // All OCaml interactions are performed here. 46 | let _ocaml_string: OCaml = "Hello, OCaml!".to_ocaml(cr); 47 | // ... invoke functions from the ocaml! macro, etc. ... 48 | }); 49 | # Ok(()) 50 | # } 51 | ``` 52 | -------------------------------------------------------------------------------- /dune-builder/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ocaml-interop-dune-builder" 3 | version = "0.12.0" 4 | edition = "2021" 5 | license = "MIT" 6 | description = "A helper crate for ocaml-interop build scripts to build OCaml code using Dune." 7 | 8 | -------------------------------------------------------------------------------- /dune-builder/README.md: -------------------------------------------------------------------------------- 1 | # OCaml Interop Dune Builder 2 | 3 | This crate provides a helper utility, `DuneBuilder`, for Rust build scripts (`build.rs`) that need to compile OCaml code using Dune and link the resulting object files. 4 | 5 | It simplifies the process of: 6 | - Locating the Dune project. 7 | - Determining relative build paths. 8 | - Invoking Dune to build a specific target. 9 | - Collecting the generated object (`.o`) files. 10 | 11 | ## Usage 12 | 13 | Add this crate as a build dependency in your `Cargo.toml`: 14 | 15 | ```toml 16 | [build-dependencies] 17 | ocaml-interop-dune-builder = "*" 18 | ``` 19 | 20 | Then, in your `build.rs` script: 21 | 22 | ```rust 23 | use ocaml_interop_dune_builder::{DuneBuilder, DuneInvocation}; 24 | 25 | fn main() { 26 | let ocaml_source_dir = "ocaml_lib"; // Directory containing your OCaml sources relative to Cargo.toml 27 | 28 | // Instruct cargo to re-run the build script if OCaml files change. 29 | // Adjust paths and filenames as necessary. 30 | println!("cargo:rerun-if-changed={}/my_ocaml_lib.ml", ocaml_source_dir); 31 | println!("cargo:rerun-if-changed={}/dune", ocaml_source_dir); // Or specific dune files 32 | 33 | let objects = DuneBuilder::new(ocaml_source_dir) 34 | .with_profile("release") // Optional: set a build profile (default is "default") 35 | .with_dune_args(vec!["--display=quiet".to_string()]) // Optional: add custom dune arguments 36 | .with_dune_invocation(DuneInvocation::System) // Optional: invoke dune directly (default is OpamExec) 37 | .build("my_ocaml_lib.exe.o"); // Or other .o target 38 | 39 | // Use a C/C++ build tool like `cc` to link the OCaml objects. 40 | let mut build = cc::Build::new(); 41 | for object_path in objects { 42 | build.object(object_path); 43 | } 44 | build.compile("name_of_your_static_library"); // e.g., "libocaml_callable" 45 | } 46 | ``` 47 | 48 | This `DuneBuilder` handles finding the dune project, constructing paths, and running the dune build command. The collected object files can then be passed to a tool like the `cc` crate to compile and link them into your Rust project. 49 | 50 | ## Configuration 51 | 52 | The `DuneBuilder` can be configured using a fluent interface: 53 | 54 | - **`new(ocaml_source_dir: P)`**: Creates a new builder. `ocaml_source_dir` is the path to your OCaml sources, relative to the `Cargo.toml` of the crate being built. 55 | - **`with_profile(profile_name: &str)`**: Sets the Dune build profile (e.g., "release", "dev"). The default is "default". 56 | - **`with_dune_args(extra_args: Vec)`**: Appends custom arguments to the `dune build` command. 57 | - **`with_dune_invocation(invocation: DuneInvocation)`**: Specifies how to call `dune`: 58 | - `DuneInvocation::OpamExec` (default): Uses `opam exec -- dune ...`. 59 | - `DuneInvocation::System`: Uses `dune ...` directly from the system path. 60 | - **`build(target: T)`**: Executes the `dune build` command for the specified `target` (e.g., `my_lib.a`, `my_exe.exe.o`) and returns a `Vec` of the compiled object files found in the build directory. 61 | -------------------------------------------------------------------------------- /dune-builder/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use std::{ 5 | path::{Path, PathBuf}, 6 | process::Command, 7 | }; 8 | 9 | /// Specifies how the `dune` command should be invoked. 10 | #[derive(Clone, Debug, PartialEq, Eq)] 11 | pub enum DuneInvocation { 12 | /// Invoke `dune` directly from the system path. 13 | System, 14 | /// Invoke `dune` via `opam exec -- dune`. This is the default. 15 | OpamExec, 16 | } 17 | 18 | fn find_dune_project_location>(base_dir: P) -> Option { 19 | let mut path = base_dir.as_ref().to_path_buf(); 20 | while let Some(parent) = path.parent() { 21 | if parent.join("dune-project").exists() { 22 | return Some(parent.to_string_lossy().into_owned()); 23 | } 24 | path = parent.to_path_buf(); 25 | } 26 | None 27 | } 28 | 29 | fn dir_relative_to_dune_project, Q: AsRef>( 30 | base_dir: P, 31 | dune_project_dir: Q, 32 | ) -> Option { 33 | let path = base_dir.as_ref(); 34 | if let Ok(relative_path) = path.strip_prefix(dune_project_dir.as_ref()) { 35 | return Some(relative_path.to_string_lossy().into_owned()); 36 | } 37 | None 38 | } 39 | 40 | /// Helper for building OCaml code with dune and collecting object files for linking with Rust. 41 | pub struct DuneBuilder { 42 | /// Absolute path to the directory containing the dune-project file. 43 | dune_project_dir: PathBuf, 44 | /// Absolute path to the dune build output directory (e.g. /_build/). 45 | dune_build_dir: PathBuf, 46 | /// Path to the OCaml source directory, relative to the dune project root (e.g. "ocaml"). 47 | ocaml_build_dir: PathBuf, 48 | /// Dune build profile (e.g., "default", "release"). 49 | profile: String, 50 | /// Custom arguments to pass to `dune build`. 51 | dune_args: Vec, 52 | /// How to invoke the `dune` command. 53 | dune_invocation: DuneInvocation, 54 | } 55 | 56 | impl DuneBuilder { 57 | /// Creates a new DuneBuilder with the "default" profile and no custom dune arguments. 58 | /// 59 | /// `ocaml_dir` is the path to the OCaml source directory relative to the crate's manifest directory. 60 | /// By default, `dune` is invoked via `opam exec -- dune`. 61 | pub fn new>(ocaml_dir: P) -> Self { 62 | let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap_or_else(|_| { 63 | panic!("CARGO_MANIFEST_DIR not set. This crate is intended for use in build scripts."); 64 | }); 65 | let callable_ocaml_dir = Path::new(&manifest_dir).join(ocaml_dir.as_ref()); 66 | 67 | let dune_project_dir_str = find_dune_project_location(&callable_ocaml_dir) 68 | .expect("Failed to find dune project location"); 69 | let dune_project_dir = PathBuf::from(dune_project_dir_str); 70 | 71 | let ocaml_build_dir_str = 72 | dir_relative_to_dune_project(&callable_ocaml_dir, &dune_project_dir) 73 | .expect("Failed to determine OCaml build directory relative to dune project"); 74 | let ocaml_build_dir = PathBuf::from(ocaml_build_dir_str); 75 | 76 | let profile = "default".to_string(); 77 | let dune_build_dir = dune_project_dir.join("_build").join(&profile); 78 | 79 | Self { 80 | dune_project_dir, 81 | dune_build_dir, 82 | ocaml_build_dir, 83 | profile, 84 | dune_args: Vec::new(), 85 | dune_invocation: DuneInvocation::OpamExec, // Default invocation 86 | } 87 | } 88 | 89 | /// Returns a new `DuneBuilder` instance with the specified build profile. 90 | /// 91 | /// The build output directory will be adjusted to `/_build/`. 92 | pub fn with_profile(self, profile_name: &str) -> Self { 93 | let new_profile = profile_name.to_string(); 94 | let new_dune_build_dir = self.dune_project_dir.join("_build").join(&new_profile); 95 | Self { 96 | dune_build_dir: new_dune_build_dir, 97 | profile: new_profile, 98 | ..self 99 | } 100 | } 101 | 102 | /// Returns a new `DuneBuilder` instance with the specified custom arguments for `dune build`. 103 | /// 104 | /// These arguments will be appended to the `dune build` command. 105 | pub fn with_dune_args(self, extra_args: Vec) -> Self { 106 | Self { 107 | dune_args: extra_args, 108 | ..self 109 | } 110 | } 111 | 112 | /// Returns a new `DuneBuilder` instance with the specified Dune invocation method. 113 | pub fn with_dune_invocation(self, invocation: DuneInvocation) -> Self { 114 | Self { 115 | dune_invocation: invocation, 116 | ..self 117 | } 118 | } 119 | 120 | /// Builds the specified dune target and returns a list of resulting object files. 121 | pub fn build>(&self, target: T) -> Vec { 122 | let target_ref = target.as_ref(); 123 | // target_path_buf is relative to ocaml_build_dir, which is relative to dune_project_dir. 124 | // This path is what `dune build` expects for the target. 125 | let target_path_for_dune = self.ocaml_build_dir.join(target_ref); 126 | let target_path_str = target_path_for_dune.to_str().unwrap_or_else(|| { 127 | panic!( 128 | "Constructed target path for dune is not valid UTF-8: {}", 129 | target_path_for_dune.display() 130 | ) 131 | }); 132 | 133 | let mut command = match self.dune_invocation { 134 | DuneInvocation::System => Command::new("dune"), 135 | DuneInvocation::OpamExec => { 136 | let mut cmd = Command::new("opam"); 137 | cmd.arg("exec").arg("--"); 138 | cmd.arg("dune"); 139 | cmd 140 | } 141 | }; 142 | 143 | command.arg("build"); 144 | 145 | if self.profile != "default" { 146 | command.arg("--profile").arg(&self.profile); 147 | } 148 | 149 | command.arg(target_path_str.to_string()); 150 | command.args(&self.dune_args); 151 | 152 | let status = command 153 | .current_dir(&self.dune_project_dir) 154 | .status() 155 | .expect("Dune build command failed to start"); 156 | 157 | if !status.success() { 158 | panic!( 159 | "Dune build failed for target: {}. Profile: {}. Args: {:?}. Exit status: {:?}", 160 | target_path_str, 161 | self.profile, 162 | self.dune_args, 163 | status.code() 164 | ); 165 | } 166 | 167 | // Find all .o files in the build output directory for the ocaml dir. 168 | // self.dune_build_dir is already profile-aware. 169 | let build_output_dir = self.dune_build_dir.join(&self.ocaml_build_dir); 170 | let mut objects = Vec::new(); 171 | match std::fs::read_dir(&build_output_dir) { 172 | Ok(entries) => { 173 | for entry in entries.flatten() { 174 | let path = entry.path(); 175 | if path.is_file() { 176 | if let Some(ext) = path.extension() { 177 | if ext == "o" { 178 | objects.push(path); 179 | } 180 | } 181 | } 182 | } 183 | } 184 | Err(e) => { 185 | panic!( 186 | "Failed to read dune build output directory '{}': {}", 187 | build_output_dir.display(), 188 | e 189 | ); 190 | } 191 | } 192 | 193 | if objects.is_empty() { 194 | eprintln!( 195 | "Warning: No .o files found in {}. Ensure the dune target '{}' produces .o files in this directory.", 196 | build_output_dir.display(), 197 | target_ref.display() 198 | ); 199 | } 200 | objects 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /dune-project: -------------------------------------------------------------------------------- 1 | (lang dune 2.6) 2 | -------------------------------------------------------------------------------- /inspect/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "ocaml-interop-inspect" 3 | version = "0.12.0" 4 | authors = ["Bruno Deferrari "] 5 | license = "MIT" 6 | description = "Runtime value inspection utilities for OCaml interop debugging" 7 | homepage = "https://github.com/tizoc/ocaml-interop" 8 | repository = "https://github.com/tizoc/ocaml-interop" 9 | keywords = ["ocaml", "rust", "ffi", "interop", "debug"] 10 | edition = "2021" 11 | 12 | [dependencies] 13 | ocaml-sys = { version = "0.26", features = ["ocaml5"] } 14 | 15 | [dev-dependencies] 16 | ocaml-interop = { path = ".." } 17 | 18 | [[example]] 19 | name = "basic_usage" 20 | path = "examples/basic_usage.rs" 21 | 22 | [[example]] 23 | name = "inspect_runtime_example" 24 | path = "examples/inspect_runtime_example/src/main.rs" 25 | required-features = ["inspect-runtime-example"] 26 | 27 | [features] 28 | default = [] 29 | inspect-runtime-example = [] 30 | -------------------------------------------------------------------------------- /inspect/examples/basic_usage.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | //! Basic usage examples of the OCaml value inspector. 5 | 6 | use ocaml_interop_inspect::inspect_raw_value; 7 | 8 | #[cfg(test)] 9 | mod tests { 10 | use super::*; 11 | 12 | #[test] 13 | fn example_inspect_integer() { 14 | // This example shows how to inspect OCaml integers 15 | let inspection = unsafe { 16 | inspect_raw_value((42_isize << 1) | 1) // OCaml integer encoding 17 | }; 18 | 19 | assert!(inspection.repr().is_immediate()); 20 | assert!(inspection.compact().contains("integer 42")); 21 | println!("Integer inspection: {}", inspection); 22 | } 23 | 24 | #[test] 25 | fn example_inspect_unit() { 26 | let inspection = unsafe { inspect_raw_value(ocaml_sys::UNIT) }; 27 | 28 | assert!(inspection.repr().is_immediate()); 29 | assert!(inspection.compact().contains("unit")); 30 | println!("Unit inspection: {}", inspection); 31 | } 32 | 33 | #[test] 34 | fn example_inspect_boolean() { 35 | let true_inspection = unsafe { inspect_raw_value(ocaml_sys::TRUE) }; 36 | 37 | let false_inspection = unsafe { inspect_raw_value(ocaml_sys::FALSE) }; 38 | 39 | assert!(true_inspection.repr().is_immediate()); 40 | assert!(false_inspection.repr().is_immediate()); 41 | assert!(true_inspection.compact().contains("true")); 42 | assert!(false_inspection.compact().contains("false")); 43 | 44 | println!("True inspection: {}", true_inspection); 45 | println!("False inspection: {}", false_inspection); 46 | } 47 | } 48 | 49 | fn main() { 50 | println!("OCaml Value Inspector Examples"); 51 | println!("=============================="); 52 | 53 | // Examples of inspecting different types of OCaml values 54 | 55 | // Integer inspection 56 | println!("1. Integer values:"); 57 | let int_inspection = unsafe { inspect_raw_value((123_isize << 1) | 1) }; 58 | println!(" {}", int_inspection); 59 | println!(" Compact: {}", int_inspection.compact()); 60 | 61 | // Boolean inspection 62 | println!("\n2. Boolean values:"); 63 | let true_inspection = unsafe { inspect_raw_value(ocaml_sys::TRUE) }; 64 | let false_inspection = unsafe { inspect_raw_value(ocaml_sys::FALSE) }; 65 | println!(" True: {}", true_inspection); 66 | println!(" False: {}", false_inspection); 67 | 68 | // Unit inspection 69 | println!("\n3. Unit value:"); 70 | let unit_inspection = unsafe { inspect_raw_value(ocaml_sys::UNIT) }; 71 | println!(" {}", unit_inspection); 72 | 73 | // Empty list inspection 74 | println!("\n4. Empty list:"); 75 | let empty_list_inspection = unsafe { inspect_raw_value(ocaml_sys::EMPTY_LIST) }; 76 | println!(" {}", empty_list_inspection); 77 | 78 | // None option inspection 79 | println!("\n5. None option:"); 80 | let none_inspection = unsafe { inspect_raw_value(ocaml_sys::NONE) }; 81 | println!(" {}", none_inspection); 82 | 83 | println!("\nFor block values (tuples, records, variants), you'll need to"); 84 | println!("create them through the OCaml runtime within a proper context."); 85 | println!("This example shows the basic immediate value inspection capabilities."); 86 | } 87 | -------------------------------------------------------------------------------- /inspect/examples/inspect_runtime_example/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "inspect-runtime-example" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | [dependencies] 8 | ocaml-interop = { path = "../../.." } 9 | ocaml-interop-inspect = { path = "../.." } 10 | 11 | [build-dependencies] 12 | cc = "1" 13 | ocaml-interop-dune-builder = { path = "../../../dune-builder" } 14 | -------------------------------------------------------------------------------- /inspect/examples/inspect_runtime_example/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use ocaml_interop_dune_builder::DuneBuilder; 5 | 6 | fn main() { 7 | let ocaml_dir = "ocaml"; 8 | 9 | // Rebuild if the OCaml source code changes 10 | println!("cargo:rerun-if-changed={}/test_types.ml", ocaml_dir); 11 | println!("cargo:rerun-if-changed={}/dune", ocaml_dir); 12 | 13 | // Build the OCaml code using dune 14 | let dune_builder = DuneBuilder::new(ocaml_dir); 15 | let objects = dune_builder.build("test_types.exe.o"); 16 | 17 | let mut build = cc::Build::new(); 18 | for object in objects { 19 | build.object(object); 20 | } 21 | 22 | build.compile("callable_ocaml"); 23 | } 24 | -------------------------------------------------------------------------------- /inspect/examples/inspect_runtime_example/ocaml/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names test_types) 3 | (libraries threads) 4 | (modes object)) 5 | -------------------------------------------------------------------------------- /inspect/examples/inspect_runtime_example/ocaml/test_types.ml: -------------------------------------------------------------------------------- 1 | (* Copyright (c) Viable Systems and TezEdge Contributors 2 | SPDX-License-Identifier: MIT *) 3 | 4 | (* Define various OCaml types to test the inspector *) 5 | 6 | (* Simple record with different field types *) 7 | type test_record = { 8 | int_field: int; 9 | float_field: float; 10 | string_field: string; 11 | bool_field: bool; 12 | tuple_field: int * string * float; 13 | } 14 | 15 | (* Variant type *) 16 | type test_variant = 17 | | Empty 18 | | WithInt of int 19 | | WithString of string 20 | | WithTuple of int * float 21 | | Complex of test_record 22 | 23 | (* List of variants *) 24 | type nested_structure = test_variant list 25 | 26 | (* Polymorphic variant *) 27 | type poly_variant = [ 28 | | `None 29 | | `Int of int 30 | | `Tuple of int * string 31 | ] 32 | 33 | (* Function to create test instances *) 34 | let create_test_record () = 35 | { 36 | int_field = 42; 37 | float_field = 3.14159; 38 | string_field = "Hello, OCaml!"; 39 | bool_field = true; 40 | tuple_field = (123, "tuple element", 45.67) 41 | } 42 | 43 | let create_empty_variant () = Empty 44 | 45 | let create_int_variant () = WithInt 42 46 | 47 | let create_string_variant () = WithString "variant string" 48 | 49 | let create_tuple_variant () = WithTuple (42, 3.14) 50 | 51 | let create_complex_variant () = Complex (create_test_record ()) 52 | 53 | let create_list_of_variants () = [ 54 | Empty; 55 | WithInt 42; 56 | WithString "variant in list"; 57 | WithTuple (99, 99.99) 58 | ] 59 | 60 | let create_poly_none () = `None 61 | 62 | let create_poly_int () = `Int 42 63 | 64 | let create_poly_tuple () = `Tuple (42, "poly tuple") 65 | 66 | (* Register all functions for Rust to call *) 67 | let () = 68 | Callback.register "create_test_record" create_test_record; 69 | Callback.register "create_empty_variant" create_empty_variant; 70 | Callback.register "create_int_variant" create_int_variant; 71 | Callback.register "create_string_variant" create_string_variant; 72 | Callback.register "create_tuple_variant" create_tuple_variant; 73 | Callback.register "create_complex_variant" create_complex_variant; 74 | Callback.register "create_list_of_variants" create_list_of_variants; 75 | Callback.register "create_poly_none" create_poly_none; 76 | Callback.register "create_poly_int" create_poly_int; 77 | Callback.register "create_poly_tuple" create_poly_tuple 78 | -------------------------------------------------------------------------------- /inspect/examples/inspect_runtime_example/src/main.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | //! This example demonstrates how to use the ocaml-interop-inspect crate 5 | //! with an actual OCaml runtime. 6 | //! 7 | //! It shows how to: 8 | //! 1. Initialize the OCaml runtime 9 | //! 2. Call OCaml functions that create various data types 10 | //! 3. Inspect the values returned from OCaml using the inspect_raw_value function 11 | //! 12 | //! The OCaml code defines various types (records, variants, lists, polymorphic variants) 13 | //! that are created and returned to Rust for inspection. 14 | //! 15 | //! For each OCaml value, the example shows: 16 | //! - The OCaml literal representation (as shown in the INSPECTING: line) 17 | //! - The detailed memory structure (blocks, tags, fields) 18 | //! - A compact view summarizing the structure 19 | //! - A debug view showing the internal representation 20 | //! 21 | //! This is useful for understanding how OCaml values are represented in memory 22 | //! and how the ocaml-interop-inspect crate helps visualize this representation. 23 | 24 | use ocaml_interop::*; 25 | use ocaml_interop_inspect::inspect_raw_value; 26 | 27 | // Define a struct to represent unknown OCaml values 28 | // This is used as a placeholder return type for OCaml functions 29 | struct UnknownOCamlValue; 30 | 31 | mod ocaml { 32 | use super::UnknownOCamlValue; 33 | use ocaml_interop::*; 34 | 35 | ocaml! { 36 | pub fn create_test_record(unit: ()) -> UnknownOCamlValue; 37 | pub fn create_empty_variant(unit: ()) -> UnknownOCamlValue; 38 | pub fn create_int_variant(unit: ()) -> UnknownOCamlValue; 39 | pub fn create_string_variant(unit: ()) -> UnknownOCamlValue; 40 | pub fn create_tuple_variant(unit: ()) -> UnknownOCamlValue; 41 | pub fn create_complex_variant(unit: ()) -> UnknownOCamlValue; 42 | pub fn create_list_of_variants(unit: ()) -> UnknownOCamlValue; 43 | pub fn create_poly_none(unit: ()) -> UnknownOCamlValue; 44 | pub fn create_poly_int(unit: ()) -> UnknownOCamlValue; 45 | pub fn create_poly_tuple(unit: ()) -> UnknownOCamlValue; 46 | } 47 | } 48 | 49 | fn print_separator() { 50 | println!("\n{}", "=".repeat(80)); 51 | } 52 | 53 | fn inspect_value(raw_value: RawOCaml, name: &str) { 54 | print_separator(); 55 | println!("INSPECTING: {}", name); 56 | println!(); 57 | 58 | // Inspect the raw OCaml value directly 59 | let inspection = unsafe { inspect_raw_value(raw_value) }; 60 | 61 | // Print the detailed representation 62 | println!("DETAILED STRUCTURE:"); 63 | println!("{}", inspection); 64 | 65 | // Print the compact view 66 | println!("\nCOMPACT VIEW:"); 67 | println!("{}", inspection.compact()); 68 | 69 | // Print the debug representation 70 | println!("\nDEBUG VIEW:"); 71 | println!("{:?}", inspection); 72 | } 73 | 74 | fn main() { 75 | // Step 1: Initialize the OCaml runtime 76 | // This starts the OCaml runtime and allows us to call OCaml functions from Rust 77 | // The guard ensures that the OCaml runtime is properly shut down when we're done 78 | let _guard = OCamlRuntime::init().expect("Failed to initialize OCaml runtime"); 79 | 80 | println!("OCaml Interop Inspect - Runtime Example"); 81 | println!("======================================"); 82 | println!("This example shows how to inspect various OCaml values using ocaml-interop-inspect."); 83 | 84 | // Step 2: Acquire the domain lock before interacting with OCaml 85 | // This is necessary for thread safety when calling OCaml functions 86 | OCamlRuntime::with_domain_lock(|cr| { 87 | // Test record 88 | let test_record = ocaml::create_test_record(cr, ()); 89 | unsafe { 90 | inspect_value(test_record.get_raw(), 91 | "{ int_field = 42; float_field = 3.14159; string_field = \"Hello, OCaml!\"; bool_field = true; tuple_field = (123, \"tuple element\", 45.67) }"); 92 | } 93 | 94 | // Empty variant 95 | let empty_variant = ocaml::create_empty_variant(cr, ()); 96 | unsafe { 97 | inspect_value(empty_variant.get_raw(), "Empty"); 98 | } 99 | 100 | // Int variant 101 | let int_variant = ocaml::create_int_variant(cr, ()); 102 | unsafe { 103 | inspect_value(int_variant.get_raw(), "WithInt 42"); 104 | } 105 | 106 | // String variant 107 | let string_variant = ocaml::create_string_variant(cr, ()); 108 | unsafe { 109 | inspect_value(string_variant.get_raw(), "WithString \"variant string\""); 110 | } 111 | 112 | // Tuple variant 113 | let tuple_variant = ocaml::create_tuple_variant(cr, ()); 114 | unsafe { 115 | inspect_value(tuple_variant.get_raw(), "WithTuple (42, 3.14)"); 116 | } 117 | 118 | // Complex variant 119 | let complex_variant = ocaml::create_complex_variant(cr, ()); 120 | unsafe { 121 | inspect_value(complex_variant.get_raw(), 122 | "Complex { int_field = 42; float_field = 3.14159; string_field = \"Hello, OCaml!\"; bool_field = true; tuple_field = (123, \"tuple element\", 45.67) }"); 123 | } 124 | 125 | // List of variants 126 | let list_of_variants = ocaml::create_list_of_variants(cr, ()); 127 | unsafe { 128 | inspect_value( 129 | list_of_variants.get_raw(), 130 | "[Empty; WithInt 42; WithString \"variant in list\"; WithTuple (99, 99.99)]", 131 | ); 132 | } 133 | 134 | // Polymorphic variants 135 | let poly_none = ocaml::create_poly_none(cr, ()); 136 | unsafe { 137 | inspect_value(poly_none.get_raw(), "`None"); 138 | } 139 | 140 | let poly_int = ocaml::create_poly_int(cr, ()); 141 | unsafe { 142 | inspect_value(poly_int.get_raw(), "`Int 42"); 143 | } 144 | 145 | let poly_tuple = ocaml::create_poly_tuple(cr, ()); 146 | unsafe { 147 | inspect_value(poly_tuple.get_raw(), "`Tuple (42, \"poly tuple\")"); 148 | } 149 | }); 150 | 151 | print_separator(); 152 | println!("Example completed successfully!"); 153 | } 154 | -------------------------------------------------------------------------------- /inspect/src/lib.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | //! # OCaml Interop Inspect 5 | //! 6 | //! This crate provides utilities for inspecting OCaml runtime values to help debug 7 | //! conversions between OCaml and Rust. It offers human-readable representations 8 | //! of OCaml values that show their internal structure including whether they are 9 | //! immediate values or blocks, their tags, sizes, and field contents. 10 | //! 11 | //! ## Usage 12 | //! 13 | //! ```rust,ignore 14 | //! use ocaml_sys::Value as RawOCaml; 15 | //! use ocaml_interop_inspect::inspect_raw_value; 16 | //! 17 | //! // When you have access to a raw OCaml value: 18 | //! unsafe { 19 | //! let raw_value: RawOCaml = /* some OCaml value */; 20 | //! let inspection = inspect_raw_value(raw_value); 21 | //! println!("OCaml value structure: {}", inspection); 22 | //! // Or for debugging output: 23 | //! println!("OCaml value structure: {:?}", inspection); 24 | //! } 25 | //! ``` 26 | //! 27 | //! When using with ocaml-interop, you can get the raw value using `value.raw()`: 28 | //! 29 | //! ```rust,ignore 30 | //! use ocaml_interop::*; 31 | //! use ocaml_interop_inspect::inspect_raw_value; 32 | //! 33 | //! fn debug_conversion(value: OCaml<'_, T>) { 34 | //! let inspection = unsafe { inspect_raw_value(value.raw()) }; 35 | //! println!("OCaml value structure: {}", inspection); 36 | //! } 37 | //! ``` 38 | 39 | // We only need RawOCaml from ocaml-sys 40 | use ocaml_sys::Value as RawOCaml; 41 | 42 | pub mod inspector; 43 | pub mod value_repr; 44 | 45 | pub use inspector::ValueInspector; 46 | pub use value_repr::ValueRepr; 47 | 48 | /// Inspect a raw OCaml value directly. 49 | /// 50 | /// This function analyzes the raw representation of an OCaml value and returns 51 | /// a `ValueInspector` that can be displayed to show the value's structure, 52 | /// including whether it's immediate or a block, its tag, size, and contents. 53 | /// 54 | /// # Example 55 | /// 56 | /// ```rust,ignore 57 | /// use ocaml_sys::Value as RawOCaml; 58 | /// use ocaml_interop_inspect::inspect_raw_value; 59 | /// 60 | /// unsafe { 61 | /// let raw_value: RawOCaml = /* some OCaml value */; 62 | /// let inspection = inspect_raw_value(raw_value); 63 | /// println!("Value structure: {}", inspection); 64 | /// } 65 | /// ``` 66 | /// 67 | /// # Safety 68 | /// 69 | /// The caller must ensure that the `RawOCaml` value is valid. 70 | pub unsafe fn inspect_raw_value(raw: RawOCaml) -> ValueInspector { 71 | ValueInspector::inspect(raw) 72 | } 73 | -------------------------------------------------------------------------------- /inspect/src/value_repr.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | //! Value representation types for OCaml value inspection. 5 | 6 | use std::fmt; 7 | 8 | /// Represents the different types of OCaml values. 9 | #[derive(Debug, Clone, PartialEq)] 10 | pub enum ValueRepr { 11 | /// An immediate value (integer, boolean, unit, etc.) 12 | Immediate { 13 | /// The raw value as an integer 14 | value: isize, 15 | /// Human-readable interpretation of the value 16 | interpretation: String, 17 | }, 18 | /// A block value (tuple, record, variant, array, etc.) 19 | Block { 20 | /// The tag of the block 21 | tag: u8, 22 | /// The size (number of fields) of the block 23 | size: usize, 24 | /// The fields of the block 25 | fields: Vec, 26 | /// Human-readable interpretation of the block type 27 | interpretation: String, 28 | }, 29 | /// A string value (special case of block with string tag) 30 | String { 31 | /// The actual string content 32 | content: String, 33 | /// The byte length of the string 34 | byte_length: usize, 35 | }, 36 | /// A custom block (used for boxed values, bigarrays, etc.) 37 | Custom { 38 | /// The tag of the custom block 39 | tag: u8, 40 | /// The size of the custom block 41 | size: usize, 42 | /// A description of what this custom block likely represents 43 | description: String, 44 | }, 45 | /// An error occurred while trying to inspect the value 46 | Error { 47 | /// Description of the error 48 | message: String, 49 | }, 50 | } 51 | 52 | impl ValueRepr { 53 | /// Get a short description of the value type 54 | pub fn type_name(&self) -> &'static str { 55 | match self { 56 | ValueRepr::Immediate { .. } => "immediate", 57 | ValueRepr::Block { .. } => "block", 58 | ValueRepr::String { .. } => "string", 59 | ValueRepr::Custom { .. } => "custom", 60 | ValueRepr::Error { .. } => "error", 61 | } 62 | } 63 | 64 | /// Check if this is an immediate value 65 | pub fn is_immediate(&self) -> bool { 66 | matches!(self, ValueRepr::Immediate { .. }) 67 | } 68 | 69 | /// Check if this is a block value 70 | pub fn is_block(&self) -> bool { 71 | matches!(self, ValueRepr::Block { .. }) 72 | } 73 | 74 | /// Check if this is a string value 75 | pub fn is_string(&self) -> bool { 76 | matches!(self, ValueRepr::String { .. }) 77 | } 78 | 79 | /// Check if this is a custom block 80 | pub fn is_custom(&self) -> bool { 81 | matches!(self, ValueRepr::Custom { .. }) 82 | } 83 | 84 | /// Check if this represents an error 85 | pub fn is_error(&self) -> bool { 86 | matches!(self, ValueRepr::Error { .. }) 87 | } 88 | } 89 | 90 | impl fmt::Display for ValueRepr { 91 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 92 | match self { 93 | ValueRepr::Immediate { 94 | value, 95 | interpretation, 96 | } => { 97 | write!(f, "Immediate({}): {}", value, interpretation) 98 | } 99 | ValueRepr::Block { 100 | tag, 101 | size, 102 | fields, 103 | interpretation, 104 | } => { 105 | write!(f, "Block(tag={}, size={}): {}", tag, size, interpretation)?; 106 | if !fields.is_empty() { 107 | write!(f, " [")?; 108 | for (i, field) in fields.iter().enumerate() { 109 | if i > 0 { 110 | write!(f, ", ")?; 111 | } 112 | write!(f, "{}: {}", i, field)?; 113 | } 114 | write!(f, "]")?; 115 | } 116 | Ok(()) 117 | } 118 | ValueRepr::String { 119 | content, 120 | byte_length, 121 | } => { 122 | // Show escaped string content, truncated if too long 123 | let display_content = if content.len() > 50 { 124 | format!("{}...", &content[..50]) 125 | } else { 126 | content.clone() 127 | }; 128 | write!(f, "String({} bytes): {:?}", byte_length, display_content) 129 | } 130 | ValueRepr::Custom { 131 | tag, 132 | size, 133 | description, 134 | } => { 135 | write!(f, "Custom(tag={}, size={}): {}", tag, size, description) 136 | } 137 | ValueRepr::Error { message } => { 138 | write!(f, "Error: {}", message) 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /inspect/tests/integration_tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use ocaml_interop_inspect::{inspect_raw_value, ValueRepr}; 5 | 6 | #[test] 7 | fn test_immediate_values() { 8 | // Test various immediate values 9 | 10 | // Integer values 11 | let int_zero = unsafe { inspect_raw_value((0_isize << 1) | 1) }; 12 | match int_zero.repr() { 13 | ValueRepr::Immediate { 14 | value, 15 | interpretation, 16 | } => { 17 | assert_eq!(*value, 1); // OCaml encoding: (0 << 1) | 1 = 1 18 | assert!(interpretation.contains("integer 0")); 19 | } 20 | _ => panic!("Expected immediate value for zero"), 21 | } 22 | 23 | let int_positive = unsafe { inspect_raw_value((42_isize << 1) | 1) }; 24 | match int_positive.repr() { 25 | ValueRepr::Immediate { 26 | value, 27 | interpretation, 28 | } => { 29 | assert_eq!(*value, 85); // OCaml encoding: (42 << 1) | 1 = 85 30 | assert!(interpretation.contains("integer 42")); 31 | } 32 | _ => panic!("Expected immediate value for 42"), 33 | } 34 | 35 | let int_negative = unsafe { inspect_raw_value((-10_isize << 1) | 1) }; 36 | match int_negative.repr() { 37 | ValueRepr::Immediate { 38 | value, 39 | interpretation, 40 | } => { 41 | assert_eq!(*value, -19); // OCaml encoding: (-10 << 1) | 1 = -19 42 | assert!(interpretation.contains("integer -10")); 43 | } 44 | _ => panic!("Expected immediate value for -10"), 45 | } 46 | } 47 | 48 | #[test] 49 | fn test_special_values() { 50 | // Test special OCaml values 51 | 52 | let unit_val = unsafe { inspect_raw_value(ocaml_sys::UNIT) }; 53 | match unit_val.repr() { 54 | ValueRepr::Immediate { interpretation, .. } => { 55 | assert!(interpretation.contains("unit")); 56 | } 57 | _ => panic!("Expected immediate value for unit"), 58 | } 59 | 60 | let true_val = unsafe { inspect_raw_value(ocaml_sys::TRUE) }; 61 | match true_val.repr() { 62 | ValueRepr::Immediate { interpretation, .. } => { 63 | assert!(interpretation.contains("true")); 64 | } 65 | _ => panic!("Expected immediate value for true"), 66 | } 67 | 68 | let false_val = unsafe { inspect_raw_value(ocaml_sys::FALSE) }; 69 | match false_val.repr() { 70 | ValueRepr::Immediate { interpretation, .. } => { 71 | assert!(interpretation.contains("false")); 72 | } 73 | _ => panic!("Expected immediate value for false"), 74 | } 75 | 76 | let empty_list = unsafe { inspect_raw_value(ocaml_sys::EMPTY_LIST) }; 77 | match empty_list.repr() { 78 | ValueRepr::Immediate { interpretation, .. } => { 79 | assert!(interpretation.contains("empty list")); 80 | } 81 | _ => panic!("Expected immediate value for empty list"), 82 | } 83 | 84 | let none_val = unsafe { inspect_raw_value(ocaml_sys::NONE) }; 85 | match none_val.repr() { 86 | ValueRepr::Immediate { interpretation, .. } => { 87 | assert!(interpretation.contains("None")); 88 | } 89 | _ => panic!("Expected immediate value for None"), 90 | } 91 | } 92 | 93 | #[test] 94 | fn test_value_type_checks() { 95 | let int_val = unsafe { inspect_raw_value((123_isize << 1) | 1) }; 96 | assert!(int_val.repr().is_immediate()); 97 | assert!(!int_val.repr().is_block()); 98 | assert!(!int_val.repr().is_string()); 99 | assert!(!int_val.repr().is_custom()); 100 | assert!(!int_val.repr().is_error()); 101 | assert_eq!(int_val.repr().type_name(), "immediate"); 102 | } 103 | 104 | #[test] 105 | fn test_compact_representation() { 106 | let int_val = unsafe { inspect_raw_value((999_isize << 1) | 1) }; 107 | let compact = int_val.compact(); 108 | 109 | // Compact representation should be concise and contain the essential info 110 | assert!(compact.contains("999")); 111 | assert!(!compact.contains('\n')); // Should be single line 112 | 113 | // Test that compact is shorter than full representation 114 | let full = format!("{}", int_val); 115 | assert!(compact.len() <= full.len()); 116 | } 117 | 118 | #[test] 119 | fn test_display_and_debug() { 120 | let int_val = unsafe { inspect_raw_value((456_isize << 1) | 1) }; 121 | 122 | // Test Display trait 123 | let display_str = format!("{}", int_val); 124 | assert!(display_str.contains("456")); 125 | 126 | // Test Debug trait 127 | let debug_str = format!("{:?}", int_val); 128 | assert!(debug_str.contains("ValueInspector")); 129 | } 130 | 131 | #[test] 132 | fn test_value_repr_display() { 133 | let int_val = unsafe { inspect_raw_value((789_isize << 1) | 1) }; 134 | let display_str = format!("{}", int_val.repr()); 135 | assert!(display_str.contains("Immediate")); 136 | assert!(display_str.contains("789")); 137 | } 138 | 139 | #[test] 140 | fn test_large_integers() { 141 | // Test edge cases for OCaml integer representation 142 | 143 | // Maximum positive value that fits in OCaml int (platform dependent, but test a reasonably large one) 144 | let large_positive = unsafe { inspect_raw_value((1000000_isize << 1) | 1) }; 145 | match large_positive.repr() { 146 | ValueRepr::Immediate { interpretation, .. } => { 147 | assert!(interpretation.contains("integer 1000000")); 148 | } 149 | _ => panic!("Expected immediate value for large positive"), 150 | } 151 | 152 | // Large negative value 153 | let large_negative = unsafe { inspect_raw_value((-500000_isize << 1) | 1) }; 154 | match large_negative.repr() { 155 | ValueRepr::Immediate { interpretation, .. } => { 156 | assert!(interpretation.contains("integer -500000")); 157 | } 158 | _ => panic!("Expected immediate value for large negative"), 159 | } 160 | } 161 | 162 | #[test] 163 | fn test_error_representation() { 164 | // Test the error representation type 165 | let error_repr = ValueRepr::Error { 166 | message: "Test error message".to_string(), 167 | }; 168 | 169 | assert!(error_repr.is_error()); 170 | assert!(!error_repr.is_immediate()); 171 | assert_eq!(error_repr.type_name(), "error"); 172 | 173 | let display_str = format!("{}", error_repr); 174 | assert!(display_str.contains("Error: Test error message")); 175 | } 176 | 177 | #[test] 178 | fn test_inspector_clone() { 179 | let int_val = unsafe { inspect_raw_value((42_isize << 1) | 1) }; 180 | let cloned = int_val.clone(); 181 | 182 | // Ensure the clone has the same representation 183 | match (int_val.repr(), cloned.repr()) { 184 | ( 185 | ValueRepr::Immediate { 186 | value: v1, 187 | interpretation: i1, 188 | }, 189 | ValueRepr::Immediate { 190 | value: v2, 191 | interpretation: i2, 192 | }, 193 | ) => { 194 | assert_eq!(v1, v2); 195 | assert_eq!(i1, i2); 196 | } 197 | _ => panic!("Expected both to be immediate values"), 198 | } 199 | } 200 | 201 | #[test] 202 | fn test_multiple_inspections() { 203 | // Test that multiple inspections of the same value produce consistent results 204 | let raw_val = (100_isize << 1) | 1; 205 | 206 | let inspect1 = unsafe { inspect_raw_value(raw_val) }; 207 | let inspect2 = unsafe { inspect_raw_value(raw_val) }; 208 | 209 | assert_eq!(format!("{}", inspect1), format!("{}", inspect2)); 210 | assert_eq!(inspect1.compact(), inspect2.compact()); 211 | } 212 | -------------------------------------------------------------------------------- /src/boxroot.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use std::{marker::PhantomData, ops::Deref}; 5 | 6 | use ocaml_boxroot_sys::{ 7 | boxroot_create, boxroot_delete, boxroot_error_string, boxroot_get, boxroot_get_ref, 8 | boxroot_modify, BoxRoot as PrimitiveBoxRoot, 9 | }; 10 | 11 | use crate::{memory::OCamlCell, OCaml, OCamlRef, OCamlRuntime}; 12 | 13 | /// `BoxRoot` is a container for a rooted [`OCaml`]`` value. 14 | pub struct BoxRoot { 15 | boxroot: PrimitiveBoxRoot, 16 | _marker: PhantomData, 17 | } 18 | 19 | fn boxroot_fail() -> ! { 20 | let reason = unsafe { std::ffi::CStr::from_ptr(boxroot_error_string()) }.to_string_lossy(); 21 | panic!("Failed to allocate boxroot, boxroot_error_string() -> {reason}"); 22 | } 23 | 24 | impl BoxRoot { 25 | /// Creates a new root from an [`OCaml`]`` value. 26 | pub fn new(val: OCaml) -> BoxRoot { 27 | if let Some(boxroot) = unsafe { boxroot_create(val.raw) } { 28 | BoxRoot { 29 | boxroot, 30 | _marker: PhantomData, 31 | } 32 | } else { 33 | boxroot_fail(); 34 | } 35 | } 36 | 37 | /// Gets the value stored in this root as an [`OCaml`]``. 38 | pub fn get<'a>(&self, cr: &'a OCamlRuntime) -> OCaml<'a, T> { 39 | unsafe { OCaml::new(cr, boxroot_get(self.boxroot)) } 40 | } 41 | 42 | /// Roots the OCaml value `val`, returning an [`OCamlRef`]``. 43 | pub fn keep<'tmp>(&'tmp mut self, val: OCaml) -> OCamlRef<'tmp, T> { 44 | unsafe { 45 | if !boxroot_modify(&mut self.boxroot, val.raw) { 46 | boxroot_fail(); 47 | } 48 | &*(boxroot_get_ref(self.boxroot) as *const OCamlCell) 49 | } 50 | } 51 | } 52 | 53 | impl Drop for BoxRoot { 54 | fn drop(&mut self) { 55 | unsafe { boxroot_delete(self.boxroot) } 56 | } 57 | } 58 | 59 | impl Deref for BoxRoot { 60 | type Target = OCamlCell; 61 | 62 | fn deref(&self) -> OCamlRef { 63 | unsafe { &*(boxroot_get_ref(self.boxroot) as *const OCamlCell) } 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod boxroot_assertions { 69 | use super::*; 70 | use static_assertions::assert_not_impl_any; 71 | 72 | // Assert that BoxRoot does not implement Send or Sync for some concrete T. 73 | // Using a simple type like () or i32 is sufficient. 74 | assert_not_impl_any!(BoxRoot<()>: Send, Sync); 75 | assert_not_impl_any!(BoxRoot: Send, Sync); 76 | } 77 | -------------------------------------------------------------------------------- /src/closure.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use crate::mlvalues::tag; 5 | use crate::mlvalues::{tag_val, RawOCaml}; 6 | use crate::value::OCaml; 7 | use crate::{OCamlRef, OCamlRuntime}; 8 | use ocaml_sys::{ 9 | caml_callback2_exn, caml_callback3_exn, caml_callbackN_exn, caml_callback_exn, caml_named_value, 10 | }; 11 | 12 | #[derive(Copy, Clone)] 13 | pub struct OCamlClosure(*const RawOCaml); 14 | 15 | unsafe impl Sync for OCamlClosure {} 16 | 17 | impl OCamlClosure { 18 | pub fn named(name: &str) -> Option { 19 | let named = unsafe { 20 | let s = match std::ffi::CString::new(name) { 21 | Ok(s) => s, 22 | Err(_) => return None, 23 | }; 24 | caml_named_value(s.as_ptr()) 25 | }; 26 | if named.is_null() || unsafe { tag_val(*named) } != tag::CLOSURE { 27 | None 28 | } else { 29 | Some(OCamlClosure(named)) 30 | } 31 | } 32 | 33 | pub fn call<'a, T, R>(&self, cr: &'a mut OCamlRuntime, arg: OCamlRef) -> OCaml<'a, R> { 34 | let result = unsafe { caml_callback_exn(*self.0, arg.get_raw()) }; 35 | self.handle_call_result(cr, result) 36 | } 37 | 38 | pub fn call2<'a, T, U, R>( 39 | &self, 40 | cr: &'a mut OCamlRuntime, 41 | arg1: OCamlRef, 42 | arg2: OCamlRef, 43 | ) -> OCaml<'a, R> { 44 | let result = unsafe { caml_callback2_exn(*self.0, arg1.get_raw(), arg2.get_raw()) }; 45 | self.handle_call_result(cr, result) 46 | } 47 | 48 | pub fn call3<'a, T, U, V, R>( 49 | &self, 50 | cr: &'a mut OCamlRuntime, 51 | arg1: OCamlRef, 52 | arg2: OCamlRef, 53 | arg3: OCamlRef, 54 | ) -> OCaml<'a, R> { 55 | let result = 56 | unsafe { caml_callback3_exn(*self.0, arg1.get_raw(), arg2.get_raw(), arg3.get_raw()) }; 57 | self.handle_call_result(cr, result) 58 | } 59 | 60 | pub fn call_n<'a, R>(&self, cr: &'a mut OCamlRuntime, args: &mut [RawOCaml]) -> OCaml<'a, R> { 61 | let len = args.len(); 62 | let result = unsafe { caml_callbackN_exn(*self.0, len, args.as_mut_ptr()) }; 63 | self.handle_call_result(cr, result) 64 | } 65 | 66 | #[inline] 67 | fn handle_call_result<'a, R>( 68 | &self, 69 | cr: &'a mut OCamlRuntime, 70 | result: RawOCaml, 71 | ) -> OCaml<'a, R> { 72 | match unsafe { OCaml::of_exception_result(cr, result) } { 73 | Some(ex) => panic!("OCaml exception, message: {:?}", ex.message()), 74 | None => unsafe { OCaml::new(cr, result) }, 75 | } 76 | } 77 | } 78 | 79 | /// OCaml function that accepts one argument. 80 | pub type OCamlFn1<'a, A, Ret> = unsafe fn(&'a mut OCamlRuntime, OCamlRef) -> OCaml<'a, Ret>; 81 | /// OCaml function that accepts two arguments. 82 | pub type OCamlFn2<'a, A, B, Ret> = 83 | unsafe fn(&'a mut OCamlRuntime, OCamlRef, OCamlRef) -> OCaml<'a, Ret>; 84 | /// OCaml function that accepts three arguments. 85 | pub type OCamlFn3<'a, A, B, C, Ret> = 86 | unsafe fn(&'a mut OCamlRuntime, OCamlRef, OCamlRef, OCamlRef) -> OCaml<'a, Ret>; 87 | /// OCaml function that accepts four arguments. 88 | pub type OCamlFn4<'a, A, B, C, D, Ret> = unsafe fn( 89 | &'a mut OCamlRuntime, 90 | OCamlRef, 91 | OCamlRef, 92 | OCamlRef, 93 | OCamlRef, 94 | ) -> OCaml<'a, Ret>; 95 | /// OCaml function that accepts five arguments. 96 | pub type OCamlFn5<'a, A, B, C, D, E, Ret> = unsafe fn( 97 | &'a mut OCamlRuntime, 98 | OCamlRef, 99 | OCamlRef, 100 | OCamlRef, 101 | OCamlRef, 102 | OCamlRef, 103 | ) -> OCaml<'a, Ret>; 104 | -------------------------------------------------------------------------------- /src/compile_fail_tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Check that OCaml values are not accessible after an allocation. 5 | // Must fail with: 6 | // error[E0499]: cannot borrow `*cr` as mutable more than once at a time 7 | /// ```compile_fail 8 | /// # use ocaml_interop::*; 9 | /// # let _guard = &mut OCamlRuntime::init().unwrap(); 10 | /// # let cr = internal::recover_runtime_handle_mut(); 11 | /// let arg1: OCaml = "test".to_owned().to_ocaml(cr); 12 | /// let arg2: OCaml = "test".to_owned().to_ocaml(cr); 13 | /// let arg1_rust: String = arg1.to_rust(); 14 | /// # () 15 | /// ``` 16 | pub struct LivenessFailureCheck; 17 | 18 | // Checks that OCamlRef values made from non-immediate OCaml values cannot be used 19 | // as if they were references to rooted values. 20 | // Must fail with: 21 | // error[E0499]: cannot borrow `*cr` as mutable more than once at a time 22 | /// ```compile_fail 23 | /// # use ocaml_interop::*; 24 | /// # ocaml! { fn ocaml_function(arg1: String) -> String; } 25 | /// # fn test(cr: &'static mut OCamlRuntime) { 26 | /// let arg1: OCaml = "test".to_ocaml(cr); 27 | /// let _ = ocaml_function(cr, &arg1); 28 | /// } 29 | /// ``` 30 | pub struct NoStaticDerefsForNonImmediates; 31 | 32 | // Must fail with: 33 | // error[E0502]: cannot borrow `*cr` as mutable because it is also borrowed as immutable 34 | /// ```compile_fail 35 | /// # use ocaml_interop::*; 36 | /// # use ocaml_interop::bigarray::Array1; 37 | /// # use std::borrow::Borrow; 38 | /// # ocaml! { pub fn ocaml_function(arg1: Array1); } 39 | /// # let _guard = &mut OCamlRuntime::init().unwrap(); 40 | /// # let cr = internal::recover_runtime_handle_mut(); 41 | /// let arr: Vec = (0..16).collect(); 42 | /// let oarr: OCaml> = arr.as_slice().to_ocaml(cr); 43 | /// let slice: &[u8] = oarr.borrow(); 44 | /// let result = ocaml_function(cr, &oarr); 45 | /// println!("{:?}", slice); 46 | /// # () 47 | pub struct BigarraySliceEscapeCheck; 48 | -------------------------------------------------------------------------------- /src/compile_ok_tests.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | mod test_immediate_ocamlrefs { 5 | // Test tests are just to confirm that AsRef for immediate OCaml values 6 | // (ints, bools, etc) compiles correctly without the need for rooting those 7 | // values. 8 | 9 | use crate::*; 10 | 11 | fn ocaml_fixnum_input(_cr: &mut OCamlRuntime, _: OCamlRef) {} 12 | fn ocaml_bool_input(_cr: &mut OCamlRuntime, _: OCamlRef) {} 13 | fn ocaml_option_input(_cr: &mut OCamlRuntime, _: OCamlRef>) {} 14 | fn ocaml_unit_input(_cr: &mut OCamlRuntime, _: OCamlRef<()>) {} 15 | 16 | fn test_immediate_ocamlref(cr: &mut OCamlRuntime) -> bool { 17 | let i: OCaml = OCaml::of_i32(10); 18 | let b: OCaml = OCaml::of_bool(true); 19 | let n: OCaml> = OCaml::none(); 20 | let u: OCaml<()> = OCaml::unit(); 21 | 22 | ocaml_fixnum_input(cr, &i); 23 | ocaml_bool_input(cr, &b); 24 | ocaml_option_input(cr, &n); 25 | ocaml_unit_input(cr, &u); 26 | 27 | true 28 | } 29 | 30 | #[test] 31 | fn test_immediate_ocamlrefs() { 32 | let cr = unsafe { crate::internal::recover_runtime_handle_mut() }; 33 | assert!(test_immediate_ocamlref(cr)); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/conv.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | mod from_ocaml; 5 | mod mapping; 6 | mod to_ocaml; 7 | 8 | pub use self::from_ocaml::FromOCaml; 9 | pub use self::mapping::{DefaultOCamlMapping, DefaultRustMapping}; 10 | pub use self::to_ocaml::ToOCaml; 11 | -------------------------------------------------------------------------------- /src/conv/mapping.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | //! Defines the `DefaultOCamlMapping` trait and implementations for default 5 | //! Rust-to-OCaml type mappings used by derive macros. 6 | 7 | use crate::{OCamlBytes, OCamlFloat, OCamlInt32, OCamlInt64, OCamlList}; 8 | 9 | /// A trait to specify the default OCaml type for a given Rust type. 10 | /// 11 | /// Derive macros like `ToOCaml` and `FromOCaml` can use this trait to infer 12 | /// the OCaml representation when an `#[ocaml(as_ = "...")]` attribute is not 13 | /// explicitly provided. 14 | pub trait DefaultOCamlMapping { 15 | /// The default OCaml type that this Rust type maps to. 16 | type OCamlType; 17 | } 18 | 19 | /// A trait to specify the default Rust type for a given OCaml type. 20 | /// 21 | /// Derive macros like `FromOCaml` can use this trait to infer 22 | /// the Rust representation when an `#[ocaml(as_ = "...")]` attribute is not 23 | /// explicitly provided for the target Rust type. 24 | pub trait DefaultRustMapping { 25 | /// The default Rust type that this OCaml type maps to. 26 | type RustType; 27 | } 28 | 29 | macro_rules! define_bidirectional_mapping { 30 | ($($rust_type:ty => $ocaml_type:ty),+ $(,)?) => { 31 | $( 32 | impl DefaultOCamlMapping for $rust_type { 33 | type OCamlType = $ocaml_type; 34 | } 35 | 36 | impl DefaultRustMapping for $ocaml_type { 37 | type RustType = $rust_type; 38 | } 39 | )+ 40 | }; 41 | } 42 | 43 | define_bidirectional_mapping! { 44 | f64 => OCamlFloat, 45 | i64 => OCamlInt64, 46 | i32 => OCamlInt32, 47 | String => String, // OCaml `string` is represented as Rust `String` 48 | bool => bool, // OCaml `bool` is represented as Rust `bool` 49 | () => (), 50 | Box<[u8]> => OCamlBytes // Box<[u8]> often maps to a byte array directly 51 | } 52 | 53 | impl DefaultOCamlMapping for &T { 54 | type OCamlType = ::OCamlType; 55 | } 56 | 57 | macro_rules! impl_bidirectional_mapping_for_tuple { 58 | ($(($($RustTypeVar:ident),+), ($($OCamlTypeVar:ident),+))+) => { 59 | $( 60 | // Rust Tuple -> OCaml Tuple 61 | impl<$($RustTypeVar),+, $($OCamlTypeVar),+> DefaultOCamlMapping for ($($RustTypeVar),+) 62 | where 63 | $($RustTypeVar: DefaultOCamlMapping),+ 64 | { 65 | type OCamlType = ($($OCamlTypeVar),+); 66 | } 67 | 68 | // OCaml Tuple -> Rust Tuple 69 | // Note: The type parameters $RustTypeVar and $OCamlTypeVar are bound at the impl level. 70 | // The where clauses ensure the correct mappings. 71 | impl<$($RustTypeVar),+, $($OCamlTypeVar),+> DefaultRustMapping for ($($OCamlTypeVar),+) 72 | where 73 | $($OCamlTypeVar: DefaultRustMapping),+ 74 | { 75 | type RustType = ($($RustTypeVar),+); 76 | } 77 | )+ 78 | }; 79 | } 80 | 81 | impl_bidirectional_mapping_for_tuple! { (A, B), (OCamlA, OCamlB) } 82 | impl_bidirectional_mapping_for_tuple! { (A, B, C), (OCamlA, OCamlB, OCamlC) } 83 | impl_bidirectional_mapping_for_tuple! { (A, B, C, D), (OCamlA, OCamlB, OCamlC, OCamlD) } 84 | impl_bidirectional_mapping_for_tuple! { (A, B, C, D, E), (OCamlA, OCamlB, OCamlC, OCamlD, OCamlE) } 85 | impl_bidirectional_mapping_for_tuple! { (A, B, C, D, E, F), (OCamlA, OCamlB, OCamlC, OCamlD, OCamlE, OCamlF) } 86 | impl_bidirectional_mapping_for_tuple! { (A, B, C, D, E, F, G), (OCamlA, OCamlB, OCamlC, OCamlD, OCamlE, OCamlF, OCamlG) } 87 | impl_bidirectional_mapping_for_tuple! { (A, B, C, D, E, F, G, H), (OCamlA, OCamlB, OCamlC, OCamlD, OCamlE, OCamlF, OCamlG, OCamlH) } 88 | impl_bidirectional_mapping_for_tuple! { (A, B, C, D, E, F, G, H, I), (OCamlA, OCamlB, OCamlC, OCamlD, OCamlE, OCamlF, OCamlG, OCamlH, OCamlI) } 89 | 90 | // --- DefaultOCamlMapping for Generic Types --- 91 | 92 | // For Box, the OCaml type is the same as T's OCaml type. 93 | // The boxing is a Rust-side memory management detail for this mapping. 94 | impl DefaultOCamlMapping for Box { 95 | type OCamlType = T::OCamlType; 96 | } 97 | 98 | // For Option, we conceptually map to an OCaml option holding T's OCaml type. 99 | // The actual conversion is handled by ToOCaml/FromOCaml impls for Option. 100 | impl DefaultOCamlMapping for Option { 101 | type OCamlType = Option; 102 | } 103 | 104 | // For Result, similar conceptual mapping. 105 | impl DefaultOCamlMapping for Result { 106 | type OCamlType = Result; 107 | } 108 | 109 | // For Vec, maps to OCamlList of T's OCaml type. 110 | // Note: Vec will map to OCamlList via this generic impl. 111 | // If OCamlBytes is desired for Vec, #[ocaml(as_ = "OCamlBytes")] should be used. 112 | impl DefaultOCamlMapping for Vec { 113 | type OCamlType = OCamlList; 114 | } 115 | 116 | // --- DefaultRustMapping for Generic OCaml Types (or conceptual OCaml types) --- 117 | 118 | // No direct structural DefaultRustMapping for the OCamlType of a generic Box 119 | // because T::OCamlType is not necessarily a "Boxed OCaml" type. 120 | // The DefaultRustMapping for T::OCamlType would yield T, and the FromOCaml derive 121 | // would handle boxing. 122 | 123 | // For OCaml's conceptual option type. T is a placeholder for an OCaml type 124 | // that implements DefaultRustMapping. 125 | impl DefaultRustMapping for Option { 126 | type RustType = Option; 127 | } 128 | 129 | // For OCaml's conceptual result type. T, E are placeholders for OCaml types. 130 | impl DefaultRustMapping for Result { 131 | type RustType = Result; 132 | } 133 | 134 | // For OCamlList, where T is a placeholder for an OCaml type. 135 | impl DefaultRustMapping for OCamlList { 136 | type RustType = Vec; 137 | } 138 | 139 | // Add more default implementations as needed 140 | -------------------------------------------------------------------------------- /src/describe.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use crate::mlvalues::{ 5 | bigarray::{Array1, BigarrayElt}, 6 | OCamlBytes, OCamlFloat, OCamlInt, OCamlInt32, OCamlInt64, OCamlList, 7 | }; 8 | use crate::{OCamlFloatArray, OCamlUniformArray}; 9 | 10 | pub trait OCamlDescriber { 11 | fn ocaml_type_name() -> String; 12 | } 13 | 14 | impl OCamlDescriber for bool { 15 | fn ocaml_type_name() -> String { 16 | "bool".to_string() 17 | } 18 | } 19 | 20 | impl OCamlDescriber for () { 21 | fn ocaml_type_name() -> String { 22 | "unit".to_string() 23 | } 24 | } 25 | 26 | impl OCamlDescriber for String { 27 | fn ocaml_type_name() -> String { 28 | "string".to_string() 29 | } 30 | } 31 | 32 | // Marker Type Implementations from `ocaml-interop` 33 | 34 | impl OCamlDescriber for OCamlInt { 35 | fn ocaml_type_name() -> String { 36 | "int".to_string() 37 | } 38 | } 39 | 40 | impl OCamlDescriber for OCamlInt32 { 41 | fn ocaml_type_name() -> String { 42 | "int32".to_string() 43 | } 44 | } 45 | 46 | impl OCamlDescriber for OCamlInt64 { 47 | fn ocaml_type_name() -> String { 48 | "int64".to_string() 49 | } 50 | } 51 | 52 | impl OCamlDescriber for OCamlFloat { 53 | fn ocaml_type_name() -> String { 54 | "float".to_string() 55 | } 56 | } 57 | 58 | impl OCamlDescriber for OCamlBytes { 59 | fn ocaml_type_name() -> String { 60 | "bytes".to_string() 61 | } 62 | } 63 | 64 | // Generic marker type implementations 65 | 66 | impl OCamlDescriber for Option { 67 | fn ocaml_type_name() -> String { 68 | format!("{} option", T::ocaml_type_name()) 69 | } 70 | } 71 | 72 | impl OCamlDescriber for Result { 73 | fn ocaml_type_name() -> String { 74 | format!( 75 | "({}, {}) result", 76 | T::ocaml_type_name(), 77 | E::ocaml_type_name() 78 | ) 79 | } 80 | } 81 | 82 | impl OCamlDescriber for OCamlList { 83 | fn ocaml_type_name() -> String { 84 | format!("{} list", T::ocaml_type_name()) 85 | } 86 | } 87 | 88 | impl OCamlDescriber for OCamlUniformArray { 89 | fn ocaml_type_name() -> String { 90 | format!("{} array", T::ocaml_type_name()) 91 | } 92 | } 93 | 94 | impl OCamlDescriber for OCamlFloatArray { 95 | fn ocaml_type_name() -> String { 96 | "float array".to_string() 97 | } 98 | } 99 | 100 | impl OCamlDescriber for Array1 { 101 | fn ocaml_type_name() -> String { 102 | // This is a simplification. OCaml's Bigarray.Array1.t type is more complex, 103 | // often showing element kind and layout. e.g. (float, float64_elt, c_layout) Bigarray.Array1.t 104 | // For now, we'll use the inner type and "bigarray". 105 | // The actual BigarrayElt might provide more info via another trait later if needed. 106 | format!("{} bigarray", T::ocaml_type_name()) 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use crate::mlvalues::{MAX_FIXNUM, MIN_FIXNUM}; 5 | use core::fmt; 6 | 7 | #[derive(Debug)] 8 | pub enum OCamlFixnumConversionError { 9 | InputTooBig(i64), 10 | InputTooSmall(i64), 11 | } 12 | 13 | impl fmt::Display for OCamlFixnumConversionError { 14 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 15 | match self { 16 | OCamlFixnumConversionError::InputTooBig(n) => write!( 17 | f, 18 | "Input value doesn't fit in OCaml fixnum n={n} > MAX_FIXNUM={MAX_FIXNUM}", 19 | ), 20 | OCamlFixnumConversionError::InputTooSmall(n) => write!( 21 | f, 22 | "Input value doesn't fit in OCaml fixnum n={n} < MIN_FIXNUM={MIN_FIXNUM}", 23 | ), 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/macros.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | #[cfg(doc)] 5 | use crate::*; 6 | 7 | /// Declares OCaml functions. 8 | /// 9 | /// `ocaml! { pub fn registered_name(arg1: ArgT, ...) -> Ret_typ; ... }` declares a function that has been 10 | /// defined in OCaml code and registered with `Callback.register "registered_name" ocaml_function`. 11 | /// 12 | /// Visibility and return value type can be omitted. The return type defaults to `()` when omitted. 13 | /// 14 | /// When invoking one of these functions, the first argument must be a `&mut `[`OCamlRuntime`], 15 | /// and the remaining arguments [`OCamlRef`]``. 16 | /// 17 | /// The return value is a [`BoxRoot`]``. 18 | /// 19 | /// Calls that raise an OCaml exception will `panic!`. Care must be taken on the OCaml side 20 | /// to avoid exceptions and return `('a, 'err) Result.t` values to signal errors, which 21 | /// can then be converted into Rust's `Result` and `Result, OCaml>`. 22 | /// 23 | /// # Examples 24 | /// 25 | /// ``` 26 | /// # use ocaml_interop::*; 27 | /// # struct MyRecord {}; 28 | /// ocaml! { 29 | /// // Declares `print_endline`, with a single `String` (`OCamlRef` when invoked) 30 | /// // argument and `BoxRoot<()>` return type (default when omitted). 31 | /// pub fn print_endline(s: String); 32 | /// 33 | /// // Declares `bytes_concat`, with two arguments, an OCaml `bytes` separator, 34 | /// // and an OCaml list of segments to concatenate. Return value is an OCaml `bytes` 35 | /// // value. 36 | /// fn bytes_concat(sep: OCamlBytes, segments: OCamlList) -> OCamlBytes; 37 | /// } 38 | /// ``` 39 | #[macro_export] 40 | macro_rules! ocaml { 41 | () => (); 42 | 43 | ($vis:vis fn $name:ident( 44 | $arg:ident: $typ:ty $(,)? 45 | ) $(-> $rtyp:ty)?; $($t:tt)*) => { 46 | $vis fn $name<'a, 'b: 'a, RustT>( 47 | cr: &'a mut $crate::OCamlRuntime, 48 | $arg: impl $crate::OCamlParam<'a, 'b, RustT, $typ>, 49 | ) -> $crate::BoxRoot<$crate::default_to_unit!($($rtyp)?)> { 50 | $crate::ocaml_closure_reference!(closure, $name); 51 | let rooted_arg = $arg.to_rooted(cr); 52 | let ocaml_ref = match rooted_arg { 53 | $crate::RefOrRooted::Ref(r) => r, 54 | $crate::RefOrRooted::Root(ref root) => &**root, 55 | }; 56 | $crate::BoxRoot::new(closure.call(cr, ocaml_ref)) 57 | } 58 | 59 | $crate::ocaml!($($t)*); 60 | }; 61 | 62 | ($vis:vis fn $name:ident( 63 | $arg1:ident: $typ1:ty, 64 | $arg2:ident: $typ2:ty $(,)? 65 | ) $(-> $rtyp:ty)?; $($t:tt)*) => { 66 | $vis fn $name<'a, 'b: 'a, RustT1, RustT2>( 67 | cr: &'a mut $crate::OCamlRuntime, 68 | $arg1: impl $crate::OCamlParam<'a, 'b, RustT1, $typ1>, 69 | $arg2: impl $crate::OCamlParam<'a, 'b, RustT2, $typ2>, 70 | ) -> $crate::BoxRoot<$crate::default_to_unit!($($rtyp)?)> { 71 | $crate::ocaml_closure_reference!(closure, $name); 72 | let rooted_arg1 = $arg1.to_rooted(cr); 73 | let rooted_arg2 = $arg2.to_rooted(cr); 74 | let ocaml_ref1 = match rooted_arg1 { 75 | $crate::RefOrRooted::Ref(r) => r, 76 | $crate::RefOrRooted::Root(ref root) => &**root, 77 | }; 78 | let ocaml_ref2 = match rooted_arg2 { 79 | $crate::RefOrRooted::Ref(r) => r, 80 | $crate::RefOrRooted::Root(ref root) => &**root, 81 | }; 82 | $crate::BoxRoot::new(closure.call2(cr, ocaml_ref1, ocaml_ref2)) 83 | } 84 | 85 | $crate::ocaml!($($t)*); 86 | }; 87 | 88 | ($vis:vis fn $name:ident( 89 | $arg1:ident: $typ1:ty, 90 | $arg2:ident: $typ2:ty, 91 | $arg3:ident: $typ3:ty $(,)? 92 | ) $(-> $rtyp:ty)?; $($t:tt)*) => { 93 | $vis fn $name<'a, 'b: 'a, RustT1, RustT2, RustT3>( 94 | cr: &'a mut $crate::OCamlRuntime, 95 | $arg1: impl $crate::OCamlParam<'a, 'b, RustT1, $typ1>, 96 | $arg2: impl $crate::OCamlParam<'a, 'b, RustT2, $typ2>, 97 | $arg3: impl $crate::OCamlParam<'a, 'b, RustT3, $typ3>, 98 | ) -> $crate::BoxRoot<$crate::default_to_unit!($($rtyp)?)> { 99 | $crate::ocaml_closure_reference!(closure, $name); 100 | let rooted_arg1 = $arg1.to_rooted(cr); 101 | let rooted_arg2 = $arg2.to_rooted(cr); 102 | let rooted_arg3 = $arg3.to_rooted(cr); 103 | let ocaml_ref1 = match rooted_arg1 { 104 | $crate::RefOrRooted::Ref(r) => r, 105 | $crate::RefOrRooted::Root(ref root) => &**root, 106 | }; 107 | let ocaml_ref2 = match rooted_arg2 { 108 | $crate::RefOrRooted::Ref(r) => r, 109 | $crate::RefOrRooted::Root(ref root) => &**root, 110 | }; 111 | let ocaml_ref3 = match rooted_arg3 { 112 | $crate::RefOrRooted::Ref(r) => r, 113 | $crate::RefOrRooted::Root(ref root) => &**root, 114 | }; 115 | $crate::BoxRoot::new(closure.call3(cr, ocaml_ref1, ocaml_ref2, ocaml_ref3)) 116 | } 117 | 118 | $crate::ocaml!($($t)*); 119 | }; 120 | 121 | ($vis:vis fn $name:ident( 122 | $($arg:ident: $typ:ty),+ $(,)? 123 | ) $(-> $rtyp:ty)?; $($t:tt)*) => { 124 | $vis fn $name<'a>( 125 | cr: &'a mut $crate::OCamlRuntime, 126 | $($arg: $crate::OCamlRef<$typ>),+ 127 | ) -> $crate::BoxRoot<$crate::default_to_unit!($($rtyp)?)> { 128 | $crate::ocaml_closure_reference!(closure, $name); 129 | $crate::BoxRoot::new(closure.call_n(cr, &mut [$(unsafe { $arg.get_raw() }),+])) 130 | } 131 | 132 | $crate::ocaml!($($t)*); 133 | } 134 | } 135 | 136 | // Internal utility macros 137 | 138 | #[doc(hidden)] 139 | #[macro_export] 140 | macro_rules! count_fields { 141 | () => {0usize}; 142 | ($_f1:ident $_f2:ident $_f3:ident $_f4:ident $_f5:ident $($fields:ident)*) => { 143 | 5usize + $crate::count_fields!($($fields)*) 144 | }; 145 | ($field:ident $($fields:ident)*) => {1usize + $crate::count_fields!($($fields)*)}; 146 | } 147 | 148 | #[doc(hidden)] 149 | #[macro_export] 150 | macro_rules! ocaml_closure_reference { 151 | ($var:ident, $name:ident) => { 152 | static NAME: &str = stringify!($name); 153 | static mut OC: Option<$crate::internal::OCamlClosure> = None; 154 | static INIT: ::std::sync::Once = ::std::sync::Once::new(); 155 | let $var = unsafe { 156 | INIT.call_once(|| { 157 | OC = $crate::internal::OCamlClosure::named(NAME); 158 | }); 159 | OC.unwrap_or_else(|| panic!("OCaml closure with name '{}' not registered", NAME)) 160 | }; 161 | }; 162 | } 163 | 164 | #[doc(hidden)] 165 | #[macro_export] 166 | macro_rules! default_to_unit { 167 | // No return value, default to unit 168 | () => { 169 | () 170 | }; 171 | 172 | // Return value specified 173 | ($rtyp:ty) => { 174 | $rtyp 175 | }; 176 | } 177 | -------------------------------------------------------------------------------- /src/memory.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | use crate::{ 5 | conv::FromOCaml, 6 | mlvalues::{ 7 | bigarray::{Array1, BigarrayElt}, 8 | tag, DynBox, OCamlBytes, OCamlFloat, OCamlInt32, OCamlInt64, OCamlList, RawOCaml, 9 | }, 10 | runtime::OCamlRuntime, 11 | value::OCaml, 12 | }; 13 | use core::{any::Any, cell::UnsafeCell, marker::PhantomData, mem, pin::Pin, ptr}; 14 | pub use ocaml_sys::{caml_alloc, store_field}; 15 | use ocaml_sys::{ 16 | caml_alloc_string, caml_alloc_tuple, caml_copy_double, caml_copy_int32, caml_copy_int64, 17 | custom_operations, string_val, Size, 18 | }; 19 | 20 | pub struct OCamlCell { 21 | cell: UnsafeCell, 22 | _marker: PhantomData, 23 | } 24 | 25 | static_assertions::assert_eq_size!(OCamlCell, OCaml<'static, bool>, RawOCaml); 26 | 27 | /// An `OCamlRef` is a reference to a location containing a [`OCaml`]`` value. 28 | /// 29 | /// Usually obtained as the result of rooting an OCaml value. 30 | pub type OCamlRef<'a, T> = &'a OCamlCell; 31 | 32 | impl OCamlCell { 33 | #[doc(hidden)] 34 | pub unsafe fn create_ref<'a>(val: *const RawOCaml) -> OCamlRef<'a, T> { 35 | &*(val as *const OCamlCell) 36 | } 37 | 38 | /// Converts this value into a Rust value. 39 | pub fn to_rust(&self, cr: &OCamlRuntime) -> RustT 40 | where 41 | RustT: FromOCaml, 42 | { 43 | RustT::from_ocaml(cr.get(self)) 44 | } 45 | 46 | /// Borrows the raw value contained in this root. 47 | /// 48 | /// # Safety 49 | /// 50 | /// The [`RawOCaml`] value obtained may become invalid after the OCaml GC runs. 51 | pub unsafe fn get_raw(&self) -> RawOCaml { 52 | *self.cell.get() 53 | } 54 | } 55 | 56 | pub fn alloc_bytes<'a>(cr: &'a mut OCamlRuntime, s: &[u8]) -> OCaml<'a, OCamlBytes> { 57 | unsafe { 58 | let len = s.len(); 59 | let value = caml_alloc_string(len); 60 | let ptr = string_val(value); 61 | core::ptr::copy_nonoverlapping(s.as_ptr(), ptr, len); 62 | OCaml::new(cr, value) 63 | } 64 | } 65 | 66 | pub fn alloc_string<'a>(cr: &'a mut OCamlRuntime, s: &str) -> OCaml<'a, String> { 67 | unsafe { 68 | let len = s.len(); 69 | let value = caml_alloc_string(len); 70 | let ptr = string_val(value); 71 | core::ptr::copy_nonoverlapping(s.as_ptr(), ptr, len); 72 | OCaml::new(cr, value) 73 | } 74 | } 75 | 76 | pub fn alloc_int32(cr: &mut OCamlRuntime, i: i32) -> OCaml { 77 | unsafe { OCaml::new(cr, caml_copy_int32(i)) } 78 | } 79 | 80 | pub fn alloc_int64(cr: &mut OCamlRuntime, i: i64) -> OCaml { 81 | unsafe { OCaml::new(cr, caml_copy_int64(i)) } 82 | } 83 | 84 | pub fn alloc_double(cr: &mut OCamlRuntime, d: f64) -> OCaml { 85 | unsafe { OCaml::new(cr, caml_copy_double(d)) } 86 | } 87 | 88 | // TODO: it is possible to directly alter the fields memory upon first allocation of 89 | // small values (like tuples and conses are) without going through `caml_modify` to get 90 | // a little bit of extra performance. 91 | 92 | pub fn alloc_some<'a, A>(cr: &'a mut OCamlRuntime, value: OCamlRef<'_, A>) -> OCaml<'a, Option> { 93 | unsafe { 94 | let ocaml_some = caml_alloc(1, tag::SOME); 95 | store_field(ocaml_some, 0, value.get_raw()); 96 | OCaml::new(cr, ocaml_some) 97 | } 98 | } 99 | 100 | pub fn alloc_ok<'a, A, Err>( 101 | cr: &'a mut OCamlRuntime, 102 | value: OCamlRef<'_, A>, 103 | ) -> OCaml<'a, Result> { 104 | unsafe { 105 | let ocaml_ok = caml_alloc(1, tag::TAG_OK); 106 | store_field(ocaml_ok, 0, value.get_raw()); 107 | OCaml::new(cr, ocaml_ok) 108 | } 109 | } 110 | 111 | pub fn alloc_error<'a, A, Err>( 112 | cr: &'a mut OCamlRuntime, 113 | err: OCamlRef<'_, Err>, 114 | ) -> OCaml<'a, Result> { 115 | unsafe { 116 | let ocaml_err = caml_alloc(1, tag::TAG_ERROR); 117 | store_field(ocaml_err, 0, err.get_raw()); 118 | OCaml::new(cr, ocaml_err) 119 | } 120 | } 121 | 122 | #[doc(hidden)] 123 | pub unsafe fn alloc_tuple(cr: &mut OCamlRuntime, size: usize) -> OCaml { 124 | let ocaml_tuple = caml_alloc_tuple(size); 125 | OCaml::new(cr, ocaml_tuple) 126 | } 127 | 128 | /// List constructor 129 | /// 130 | /// Build a new list from a head and a tail list. 131 | pub fn alloc_cons<'a, 'b, A>( 132 | cr: &'a mut OCamlRuntime, 133 | head: OCamlRef<'b, A>, 134 | tail: OCamlRef<'b, OCamlList>, 135 | ) -> OCaml<'a, OCamlList> { 136 | unsafe { 137 | let ocaml_cons = caml_alloc(2, tag::CONS); 138 | store_field(ocaml_cons, 0, head.get_raw()); 139 | store_field(ocaml_cons, 1, tail.get_raw()); 140 | OCaml::new(cr, ocaml_cons) 141 | } 142 | } 143 | 144 | #[inline] 145 | pub unsafe fn store_raw_field_at( 146 | cr: &mut OCamlRuntime, 147 | block: OCamlRef, 148 | offset: Size, 149 | raw_value: RawOCaml, 150 | ) { 151 | store_field(cr.get(block).get_raw(), offset, raw_value); 152 | } 153 | 154 | const BOX_OPS_DYN_DROP: custom_operations = custom_operations { 155 | identifier: c"_rust_box_dyn_drop".as_ptr() as *const ocaml_sys::Char, 156 | finalize: Some(drop_box_dyn), 157 | compare: None, 158 | hash: None, 159 | serialize: None, 160 | deserialize: None, 161 | compare_ext: None, 162 | fixed_length: ptr::null(), 163 | }; 164 | 165 | extern "C" fn drop_box_dyn(oval: RawOCaml) { 166 | unsafe { 167 | let box_ptr = ocaml_sys::field(oval, 1) as *mut Pin>; 168 | ptr::drop_in_place(box_ptr); 169 | } 170 | } 171 | 172 | // Notes by @g2p: 173 | // 174 | // Implementation notes: is it possible to reduce indirection? 175 | // Could we also skip the finalizer? 176 | // 177 | // While putting T immediately inside the custom block as field(1) 178 | // is tempting, GC would misalign it (UB) when moving. Put a pointer to T instead. 179 | // That optimisation would only work when alignment is the same as OCaml, 180 | // meaning size_of. It would also need to use different types. 181 | // 182 | // Use Any for now. This allows safe downcasting when converting back to Rust. 183 | // 184 | // mem::needs_drop can be used to detect drop glue. 185 | // This could be used to skip the finalizer, but only when there's no box. 186 | // Using a lighter finalizer won't work either, the GlobalAllocator trait needs 187 | // to know the layout before freeing the referenced block. 188 | // malloc won't use that info, but other allocators would. 189 | // 190 | // Also: caml_register_custom_operations is only useful for Marshall serialization, 191 | // skip it 192 | 193 | /// Allocate a `DynBox` for a value of type `A`. 194 | pub fn alloc_box(cr: &mut OCamlRuntime, data: A) -> OCaml> { 195 | let oval; 196 | // A fatter Box, points to data then to vtable 197 | type B = Pin>; 198 | unsafe { 199 | oval = ocaml_sys::caml_alloc_custom(&BOX_OPS_DYN_DROP, mem::size_of::(), 0, 1); 200 | let box_ptr = ocaml_sys::field(oval, 1) as *mut B; 201 | std::ptr::write(box_ptr, Box::pin(data)); 202 | } 203 | unsafe { OCaml::new(cr, oval) } 204 | } 205 | 206 | /// Create a new OCaml `Bigarray.Array1` with the given type and size 207 | /// 208 | /// Memory belongs to the OCaml GC, 209 | /// including the data, which is in the malloc heap but will be freed on 210 | /// collection through a custom block 211 | pub fn alloc_bigarray1<'a, A: BigarrayElt>( 212 | cr: &'a mut OCamlRuntime, 213 | data: &[A], 214 | ) -> OCaml<'a, Array1> { 215 | let len = data.len(); 216 | let ocaml_ba; 217 | unsafe { 218 | // num_dims == 1 219 | // data == NULL, OCaml will allocate with malloc (outside the GC) 220 | // and add the CAML_BA_MANAGED flag 221 | // OCaml custom block contains a bigarray struct after the header, 222 | // that points to the data array 223 | ocaml_ba = ocaml_sys::bigarray::caml_ba_alloc_dims(A::KIND, 1, core::ptr::null_mut(), len); 224 | let ba_meta_ptr = ocaml_sys::field(ocaml_ba, 1) as *const ocaml_sys::bigarray::Bigarray; 225 | core::ptr::copy_nonoverlapping(data.as_ptr(), (*ba_meta_ptr).data as *mut A, len); 226 | } 227 | unsafe { OCaml::new(cr, ocaml_ba) } 228 | } 229 | -------------------------------------------------------------------------------- /src/mlvalues.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | #[cfg(doc)] 5 | use crate::*; 6 | 7 | use core::marker::PhantomData; 8 | pub use ocaml_sys::{ 9 | extract_exception, field as field_val, is_block, is_exception_result, is_long, string_val, 10 | tag_val, wosize_val, Intnat, Uintnat as UIntnat, Value as RawOCaml, EMPTY_LIST, FALSE, 11 | MAX_FIXNUM, MIN_FIXNUM, NONE, TRUE, UNIT, 12 | }; 13 | 14 | pub mod bigarray; 15 | pub mod tag; 16 | 17 | pub(crate) unsafe fn int32_val(v: RawOCaml) -> i32 { 18 | let val = unsafe { field_val(v, 1) }; 19 | unsafe { *(val as *const i32) } 20 | } 21 | 22 | pub(crate) unsafe fn int64_val(v: RawOCaml) -> i64 { 23 | let val = unsafe { field_val(v, 1) }; 24 | unsafe { *(val as *const i64) } 25 | } 26 | 27 | /// [`OCaml`]`>` is a reference to an OCaml `list` containing 28 | /// values of type `T`. 29 | pub struct OCamlList { 30 | _marker: PhantomData, 31 | } 32 | 33 | /// [`OCaml`]`>` is a reference to an OCaml array which is 34 | /// guaranteed to not contain unboxed floats. If OCaml was configured with 35 | /// `--disable-flat-float-array` this corresponds to regular `array`s, but if 36 | /// not, `Uniform_array.t` in the `base` library can be used instead. 37 | /// See [Lexifi's blog post on the topic](https://www.lexifi.com/blog/ocaml/about-unboxed-float-arrays/) 38 | /// for more details. 39 | pub struct OCamlUniformArray { 40 | _marker: PhantomData, 41 | } 42 | 43 | /// [`OCaml`]`>` is a reference to an OCaml `floatarray` 44 | /// which is an array containing `float`s in an unboxed form. 45 | pub struct OCamlFloatArray {} 46 | 47 | /// `OCaml>` is for passing a value of type `T` to OCaml 48 | /// 49 | /// To box a Rust value, use [`OCaml::box_value`][crate::OCaml::box_value]. 50 | /// 51 | /// **Experimental** 52 | pub struct DynBox { 53 | _marker: PhantomData, 54 | } 55 | 56 | /// [`OCaml`]`` is a reference to an OCaml `bytes` value. 57 | /// 58 | /// # Note 59 | /// 60 | /// Unlike with [`OCaml`]``, there is no validation being performed when converting this 61 | /// value into `String`. 62 | pub struct OCamlBytes {} 63 | 64 | /// [`OCaml`]`` is an OCaml integer (tagged and unboxed) value. 65 | pub type OCamlInt = Intnat; 66 | 67 | /// [`OCaml`]`` is a reference to an OCaml `Int32.t` (boxed `int32`) value. 68 | pub struct OCamlInt32 {} 69 | 70 | /// [`OCaml`]`` is a reference to an OCaml `Int64.t` (boxed `int64`) value. 71 | pub struct OCamlInt64 {} 72 | 73 | /// [`OCaml`]`` is a reference to an OCaml `float` (boxed `float`) value. 74 | pub struct OCamlFloat {} 75 | 76 | /// [`OCaml`]`` is a reference to an OCaml `exn` value. 77 | pub struct OCamlException {} 78 | -------------------------------------------------------------------------------- /src/mlvalues/bigarray.rs: -------------------------------------------------------------------------------- 1 | use core::marker::PhantomData; 2 | 3 | /// Bigarray kind 4 | /// 5 | /// # Safety 6 | /// 7 | /// This is unsafe to implement, because it allows casts 8 | /// to the implementing type (through `OCaml>::as_slice()`). 9 | /// 10 | /// To make this safe, the type implementing this trait must be 11 | /// safe to transmute from OCaml data with the relevant KIND. 12 | // 13 | // Using a trait for this means a Rust type can only be matched to a 14 | // single kind. There are enough Rust types that this isn't a problem 15 | // in practice. 16 | pub unsafe trait BigarrayElt: Copy { 17 | /// OCaml bigarray type identifier 18 | const KIND: i32; 19 | } 20 | 21 | // TODO: 22 | // assert that size_of::<$t>() matches caml_ba_element_size[$k] 23 | // Not sure we can check this at compile time, 24 | // when caml_ba_element_size is a C symbol 25 | macro_rules! make_kind { 26 | ($t:ty, $k:ident) => { 27 | unsafe impl BigarrayElt for $t { 28 | const KIND: i32 = ocaml_sys::bigarray::Kind::$k as i32; 29 | } 30 | }; 31 | } 32 | 33 | // In kind order 34 | // Skips some kinds OCaml supports: caml_int, complex32, complex64 35 | make_kind!(f32, FLOAT32); 36 | make_kind!(f64, FLOAT64); 37 | make_kind!(i8, SINT8); 38 | make_kind!(u8, UINT8); 39 | make_kind!(i16, SINT16); 40 | make_kind!(u16, UINT16); 41 | make_kind!(i32, INT32); 42 | make_kind!(i64, INT64); 43 | make_kind!(isize, NATIVE_INT); 44 | make_kind!(char, CHAR); 45 | 46 | pub struct Array1 { 47 | _marker: PhantomData, 48 | } 49 | -------------------------------------------------------------------------------- /src/mlvalues/tag.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | pub use ocaml_sys::{ 5 | Tag, CLOSURE, DOUBLE_ARRAY, NO_SCAN, STRING, TAG_CONS as CONS, TAG_SOME as SOME, 6 | }; 7 | 8 | pub const TAG_POLYMORPHIC_VARIANT: Tag = 0; 9 | 10 | /// Note that `TAG_EXCEPTION`` is equivalent to `TAG_POLYMORPHIC_VARIANT`, and also 11 | /// corresponds to the tag associated with records and tuples. 12 | pub const TAG_EXCEPTION: Tag = 0; 13 | 14 | pub const TAG_OK: Tag = 0; 15 | pub const TAG_ERROR: Tag = 1; 16 | -------------------------------------------------------------------------------- /src/user_guides.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | #[allow(unused_imports)] 5 | use crate::*; 6 | 7 | #[doc = include_str!("../docs/part1-initial-usage-a-brief-overview.md")] 8 | pub mod part1_initial_usage_a_brief_overview {} 9 | 10 | #[doc = include_str!("../docs/part2-fundamental-concepts.md")] 11 | pub mod part2_fundamental_concepts {} 12 | 13 | #[doc = include_str!("../docs/part3-exporting-rust-functions-to-ocaml.md")] 14 | pub mod part3_exporting_rust_functions_to_ocaml {} 15 | 16 | #[doc = include_str!("../docs/part4-invoking-ocaml-functions-from-rust.md")] 17 | pub mod part4_invoking_ocaml_functions_from_rust {} 18 | 19 | #[doc = include_str!("../docs/part5-managing-the-ocaml-runtime-for-rust-driven-programs.md")] 20 | pub mod part5_managing_the_ocaml_runtime_for_rust_driven_programs {} 21 | 22 | #[doc = include_str!("../docs/part6-advanced-topics.md")] 23 | pub mod part6_advanced_topics {} 24 | 25 | #[doc = include_str!("../docs/part7-build-and-link-instructions.md")] 26 | pub mod part7_build_and_link_instructions {} 27 | -------------------------------------------------------------------------------- /testing/ocaml-caller/.ocamlformat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tizoc/ocaml-interop/9f941c63b8a9dcd652c6535da196f48d7a4dcc75/testing/ocaml-caller/.ocamlformat -------------------------------------------------------------------------------- /testing/ocaml-caller/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names ocaml_rust_caller) 3 | (libraries alcotest base callable_rust threads.posix)) 4 | 5 | (rule 6 | (alias runtest) 7 | (action 8 | (run ./ocaml_rust_caller.exe))) 9 | -------------------------------------------------------------------------------- /testing/ocaml-caller/rust/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "callable_rust" 3 | version = "0.1.0" 4 | authors = ["Bruno Deferrari "] 5 | edition = "2021" 6 | publish = false 7 | 8 | [lib] 9 | crate-type = ["staticlib", "cdylib"] 10 | 11 | [dependencies] 12 | ocaml-interop = { path = "../../.." } 13 | -------------------------------------------------------------------------------- /testing/ocaml-caller/rust/dune: -------------------------------------------------------------------------------- 1 | (rule 2 | (targets libcallable_rust.a dllcallable_rust.so) 3 | (deps (source_tree src) dune-Cargo.toml) 4 | (action 5 | (no-infer 6 | (progn 7 | ;; macOS requires these flags because undefined symbols are not allowed by default 8 | (run sh -c " 9 | if [ \"$(uname -s)\" = \"Darwin\" ]; then 10 | export RUSTFLAGS='-C link-args=-Wl,-undefined,dynamic_lookup' 11 | fi 12 | mv dune-Cargo.toml Cargo.toml 13 | cargo build 14 | ") 15 | (run sh -c 16 | "cp target/debug/libcallable_rust.so ./dllcallable_rust.so 2> /dev/null || \ 17 | cp target/debug/libcallable_rust.dylib ./dllcallable_rust.so") 18 | (run cp target/debug/libcallable_rust.a ./libcallable_rust.a) 19 | )))) 20 | 21 | (library 22 | (name callable_rust) 23 | (c_library_flags -lc -lm) 24 | (foreign_archives callable_rust)) -------------------------------------------------------------------------------- /testing/ocaml-caller/rust/dune-Cargo.toml: -------------------------------------------------------------------------------- 1 | # To be used by `dune` where the relative path 2 | # to ocaml-interop is different and we don't 3 | # want to be inside the toplevel workspace. 4 | 5 | [package] 6 | name = "callable_rust" 7 | version = "0.1.0" 8 | authors = ["Bruno Deferrari "] 9 | edition = "2021" 10 | 11 | [lib] 12 | crate-type = ["staticlib", "cdylib"] 13 | 14 | [dependencies] 15 | ocaml-interop = { path = "../../../../.." } 16 | 17 | [workspace] 18 | -------------------------------------------------------------------------------- /testing/rust-caller/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "rust-caller" 3 | version = "0.1.0" 4 | authors = ["Bruno Deferrari "] 5 | edition = "2021" 6 | publish = false 7 | 8 | [dependencies] 9 | ocaml-interop = { path = "../.." } 10 | 11 | [build-dependencies] 12 | cc = "1" 13 | ocaml-interop-dune-builder = { path = "../../dune-builder" } -------------------------------------------------------------------------------- /testing/rust-caller/build.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) Viable Systems and TezEdge Contributors 2 | // SPDX-License-Identifier: MIT 3 | 4 | // DuneBuilder is a helper for building OCaml code with dune and collecting object 5 | // files for linking with Rust. 6 | use ocaml_interop_dune_builder::DuneBuilder; 7 | 8 | fn main() { 9 | // Relative path to the OCaml source directory 10 | // It is used to determine the location of the dune project and the build directory. 11 | // The path is relative to the crate's manifest directory. 12 | // In this case, it is "ocaml", which is the directory containing the OCaml code. 13 | let ocaml_callable_dir = "ocaml"; 14 | 15 | // Rebuild if the OCaml source code changes. 16 | println!("cargo:rerun-if-changed={}/callable.ml", ocaml_callable_dir); 17 | println!("cargo:rerun-if-changed={}/dune", ocaml_callable_dir); 18 | 19 | // Build the OCaml code using dune and collect the object files for linking with Rust. 20 | let dune_builder = DuneBuilder::new(ocaml_callable_dir); 21 | let objects = dune_builder.build("callable.exe.o"); 22 | 23 | let mut build = cc::Build::new(); 24 | for object in objects { 25 | build.object(object); 26 | } 27 | 28 | build.compile("callable_ocaml"); 29 | } 30 | -------------------------------------------------------------------------------- /testing/rust-caller/ocaml/callable.ml: -------------------------------------------------------------------------------- 1 | (* Copyright (c) Viable Systems and TezEdge Contributors 2 | SPDX-License-Identifier: MIT *) 3 | 4 | type test_record = { 5 | i: int; 6 | f: float; 7 | i32: int32; 8 | i64: int64; 9 | s: string; 10 | t: int * float; 11 | } 12 | 13 | type movement = 14 | | Step of int 15 | | RotateLeft 16 | | RotateRight 17 | 18 | type polymorphic_enum = [ 19 | | `Unit 20 | | `Single of float 21 | | `Multiple of (int * string) 22 | ] 23 | 24 | let increment_bytes bytes first_n = 25 | let limit = (min (Bytes.length bytes) first_n) - 1 in 26 | for i = 0 to limit do 27 | let value = (Bytes.get_uint8 bytes i) + 1 in 28 | Bytes.set_uint8 bytes i value 29 | done; 30 | bytes 31 | 32 | let decrement_bytes bytes first_n = 33 | let limit = (min (Bytes.length bytes) first_n) - 1 in 34 | for i = 0 to limit do 35 | let value = (Bytes.get_uint8 bytes i) - 1 in 36 | Bytes.set_uint8 bytes i value 37 | done; 38 | bytes 39 | 40 | let increment_ints_list ints = 41 | List.map ((+) 1) ints 42 | 43 | let twice x = 2 * x 44 | 45 | let make_tuple a b = (a, b) 46 | 47 | let make_some x = Some x 48 | 49 | let make_ok x = Ok x 50 | 51 | let make_error x = Error x 52 | 53 | let stringify_record { i; f; i32; i64; s; t = (t1, t2); } = 54 | Printf.sprintf "{ i=%d; f=%.2f; i32=%ld; i64=%Ld; s=%s; t=(%d, %.2f) }" 55 | i f i32 i64 s t1 t2 56 | 57 | let stringify_variant = function 58 | | RotateLeft -> "RotateLeft" 59 | | RotateRight -> "RotateRight" 60 | | Step n -> Printf.sprintf "Step(%d)" n 61 | 62 | let stringify_polymorphic_variant = function 63 | | `Single n -> Printf.sprintf "Single(%.2f)" n 64 | | `Multiple (n, s) -> Printf.sprintf "Multiple(%d, %s)" n s 65 | | `Unit -> "Unit" 66 | 67 | let raises_message_exception msg = failwith msg 68 | 69 | let raises_nonblock_exception () = raise Not_found 70 | 71 | exception WithInt of int 72 | 73 | let raises_nonmessage_exception () = raise (WithInt 10) 74 | 75 | let reverse_list_and_compact l = 76 | let r = List.rev l in 77 | Gc.compact (); 78 | r 79 | 80 | let double_u16_array arr = 81 | let open Bigarray in 82 | let n = Array1.dim arr in 83 | for i = 0 to pred n do 84 | Array1.(set arr i ((get arr i) * 2)) 85 | done 86 | 87 | 88 | let () = 89 | Callback.register "increment_bytes" increment_bytes; 90 | Callback.register "decrement_bytes" decrement_bytes; 91 | Callback.register "increment_ints_list" increment_ints_list; 92 | Callback.register "twice" twice; 93 | Callback.register "make_tuple" make_tuple; 94 | Callback.register "make_some" make_some; 95 | Callback.register "make_ok" make_ok; 96 | Callback.register "make_error" make_error; 97 | Callback.register "stringify_record" stringify_record; 98 | Callback.register "stringify_variant" stringify_variant; 99 | Callback.register "stringify_polymorphic_variant" stringify_polymorphic_variant; 100 | Callback.register "raises_message_exception" raises_message_exception; 101 | Callback.register "raises_nonmessage_exception" raises_nonmessage_exception; 102 | Callback.register "raises_nonblock_exception" raises_nonblock_exception; 103 | Callback.register "gc_compact" Gc.compact; 104 | Callback.register "reverse_list_and_compact" reverse_list_and_compact; 105 | Callback.register "double_u16_array" double_u16_array; 106 | -------------------------------------------------------------------------------- /testing/rust-caller/ocaml/dune: -------------------------------------------------------------------------------- 1 | (executables 2 | (names callable) 3 | (libraries threads) 4 | (modes object)) --------------------------------------------------------------------------------