From 3bb07b14aa43afa4940184fd36e4d7455edbc529 Mon Sep 17 00:00:00 2001 From: zach Date: Sun, 29 May 2022 11:16:11 -0700 Subject: [PATCH] Remove examples from README --- Makefile | 2 + README.md | 207 +----------------- doc/src/01_introduction.md | 35 +++ doc/src/02_type_conversion.md | 5 + doc/src/03_writing_ocaml_functions_in_rust.md | 121 +++++++++- src/lib.rs | 2 + 6 files changed, 168 insertions(+), 204 deletions(-) diff --git a/Makefile b/Makefile index 0ef6e3a2..8b244809 100644 --- a/Makefile +++ b/Makefile @@ -8,6 +8,8 @@ test-ocaml: @dune runtest --root=test --force --no-buffer test-book: + @cargo clean + @cargo build @mdbook test doc -L ./target/debug/deps build-book: diff --git a/README.md b/README.md index 858fce07..a0664ea2 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,12 @@ Works with OCaml versions `4.06.0` and up Please report any issues on [github](https://github.com/zshipko/ocaml-rs/issues) -NOTE: While `ocaml-rs` *can* be used safely, it does not prevent a wide range of potential errors or mistakes. It should be thought of as a Rust implementation of the existing C API. [ocaml-interop](https://github.com/simplestaking/ocaml-interop) can be used to perform safe OCaml/Rust interop. The latest version of `ocaml-rs` actually uses `ocaml-interop` behind the scenes to interact with the garbage collector. `ocaml-rs` also exports an `interop` module, which is an alias for `ocaml_interop` and the two interfaces can be combined if desired. +NOTE: While `ocaml-rs` *can* be used safely, it does not prevent a wide range of potential errors or mistakes. It should be thought of as a Rust implementation of the existing C API. [ocaml-interop](https://github.com/simplestaking/ocaml-interop) can be used to perform safe OCaml/Rust interop. `ocaml-rs` uses `ocaml-interop` behind the scenes to interact with the garbage collector. `ocaml-rs` also exports an `interop` module, which is an alias for `ocaml_interop` and the two interfaces can be combined if desired. + +### Documentation + +- [ocaml-rs Book](https://zshipko.github.io/ocaml-rs) +- [docs.rs](https://docs.rs/ocaml) ### Getting started @@ -68,204 +73,4 @@ Defining the `OCAML_VERSION` and `OCAML_WHERE_PATH` variables is useful for savi - `no-std` * Allows `ocaml` to be used in `#![no_std]` environments like MirageOS -### Documentation - -[https://docs.rs/ocaml](https://docs.rs/ocaml) - -### Examples - -```rust -// Automatically derive `ToValue` and `FromValue` -#[derive(ocaml::ToValue, ocaml::FromValue)] -#[ocaml::sig("{name: string; i: int}")] -struct Example<'a> { - name: &'a str, - i: ocaml::Int, -} - - -#[ocaml::func] -#[ocaml::sig("example -> example")] -pub fn incr_example(mut e: Example) -> Example { - e.i += 1; - e -} - -#[ocaml::func] -#[ocaml::sig("int -> int * int * int")] -pub fn build_tuple(i: ocaml::Int) -> (ocaml::Int, ocaml::Int, ocaml::Int) { - (i + 1, i + 2, i + 3) -} - -#[ocaml::func] -#[ocaml::sig("float array -> f64")] -pub fn average(arr: ocaml::Array) -> Result { - let mut sum = 0f64; - - for i in 0..arr.len() { - sum += arr.get_f64(i)?; - } - - Ok(sum / arr.len() as f64) -} - -// A `native_func` must take `ocaml::Value` for every argument and return an `ocaml::Value` -// these functions have minimal overhead compared to wrapping with `func` -#[ocaml::native_func] -pub fn incr(value: ocaml::Value) -> ocaml::Value { - let i = value.int_val(); - ocaml::Value::int(i + 1) -} - -// This is equivalent to: -#[no_mangle] -pub extern "C" fn incr2(value: ocaml::Value) -> ocaml::Value { - ocaml::body!(gc: (value) { - let i = value.int_val(); - ocaml::Value::int( i + 1) - }) -} - -// `ocaml::native_func` is responsible for: -// - Ensures that #[no_mangle] and extern "C" are added, in addition to wrapping -// - Wraps the function body using `ocaml::body!` - -// Finally, if your function is marked [@@unboxed] and [@@noalloc] in OCaml then you can avoid -// boxing altogether for f64 arguments using a plain C function and a bytecode function -// definition: -#[no_mangle] -pub extern "C" fn incrf(input: f64) -> f64 { - input + 1.0 -} - -#[cfg(feature = "derive")] -#[ocaml::bytecode_func] -pub fn incrf_bytecode(input: f64) -> f64 { - incrf(input) -} -``` - -Note: By default the `func` macro will create a bytecode wrapper (using `bytecode_func`) for functions with more than 5 arguments. - -The OCaml stubs would look like this: - -```ocaml -type example = { - name: string; - i: int; -} - -external incr_example: example -> example = "incr_example" -external build_tuple: int -> int * int * int = "build_tuple" -external average: float array -> float = "average" -external incr: int -> int = "incr" -external incr2: int -> int = "incr2" -external incrf: float -> float = "incrf_bytecode" "incrf" [@@unboxed] [@@noalloc] -``` - -Excluding the `incrf` example, these can also be generated using [ocaml-build](https://github.com/zshipko/ocaml-rs/blob/master/build/README.md) - -For more examples see [test/src](https://github.com/zshipko/ocaml-rs/blob/master/test/src) or [ocaml-vec](https://github.com/zshipko/ocaml-vec). - -### Type conversion - -This chart contains the mapping between Rust and OCaml types used by `ocaml::func` - -| Rust type | OCaml type | -| ------------------------- | -------------------- | -| `()` | `unit` | -| `isize` | `int` | -| `usize` | `int` | -| `i8` | `int` | -| `u8` | `int` | -| `i16` | `int` | -| `u16` | `int` | -| `i32` | `int32` | -| `u32` | `int32` | -| `i64` | `int64` | -| `u64` | `int64` | -| `f32` | `float` | -| `f64` | `float` | -| `str` | `string` | -| `[u8]` | `bytes` | -| `String` | `string` | -| `Option` | `'a option` | -| `Result` | `exception` | -| `Result` | `('a, 'b) Result.t` | -| `(A, B, C)` | `'a * 'b * 'c` | -| `&[Value]` | `'a array` (no copy) | -| `Vec`, `&[A]` | `'a array` | -| `BTreeMap` | `('a, 'b) list` | -| `LinkedList` | `'a list` | - -NOTE: Even though `&[Value]` is specifically marked as no copy, any type like `Option` would also qualify since the inner value is not converted to a Rust type. However, `Option` will do full unmarshaling into Rust types. Another thing to note: `FromValue` for `str` and `&[u8]` is zero-copy, however `ToValue` for `str` and `&[u8]` creates a new value - this is necessary to ensure the string is registered with the OCaml runtime. - -If you're concerned with minimizing allocations/conversions you should use `Value` type directly. - -#### Pointers to Rust values on the OCaml heap - -`Pointer` can be used to create and access Rust types on the OCaml heap. - -For example, for a type that implements `Custom`: - -```rust -use ocaml::FromValue; - -struct MyType; - -unsafe extern "C" fn mytype_finalizer(v: ocaml::Raw) { - let ptr = v.as_pointer::(); - ptr.drop_in_place() -} - -ocaml::custom_finalize!(MyType, mytype_finalizer); - -#[ocaml::func] -pub fn new_my_type() -> ocaml::Pointer { - ocaml::Pointer::alloc_custom(gc, MyType) - // ocaml::Pointer::alloc_final(gc, MyType, finalizer, None) can also be used - // if you don't intend to implement `Custom` -} - -#[ocaml::func] -pub fn my_type_example(t: ocaml::Pointer) { - let my_type = t.as_mut(); - // MyType has no fields, but normally you - // would do something with MyType here -} -``` - -#### Custom exception type - -When a Rust `panic` or `Err` is encountered it will be raised as a `Failure` on the OCaml side, to configure a custom exception type you can register it with the OCaml runtime using the name `Rust_exception`: - -```ocaml -exception Rust - -let () = Callback.register_exception "Rust_error" (Rust "") -``` - -It must take a single `string` argument. - -#### Using `import!` macro - -The `ocaml::import!` macro can be used to call OCaml functions that have been registered using `Callback.register` from Rust: - -```rust -ocaml::import! { - fn my_function(i: ocaml::Int) -> ocaml::Int; -} -``` - -Which can then be called directly: - -```rust -my_function(&rt, 123) -``` - -### `ocaml::sig` macro - -The `ocaml::sig` macro in combination with `ocaml-build` can be used to write signatures for OCaml functions and types in-line. - -See [build/README.md](https://github.com/zshipko/ocaml-rs/blob/master/build/README.md) diff --git a/doc/src/01_introduction.md b/doc/src/01_introduction.md index ed4e4a18..0bfd9181 100644 --- a/doc/src/01_introduction.md +++ b/doc/src/01_introduction.md @@ -28,6 +28,8 @@ Additionally, on macOS you may need to add a `.cargo/config` with the following: rustflags = ["-C", "link-args=-Wl,-undefined,dynamic_lookup"] ``` +This is because macOS doesn't allow undefined symbols in dynamic libraries by default. + If you plan on using `ocaml-build`: ```toml @@ -67,8 +69,41 @@ Next you will need to add setup a [dune](https://dune.build) project to handle c It can take a little trial and error to get this right depending on the specifics of your project! +Additionally, if you plan on releasing to [opam](https://github.com/ocaml/opam), you will need to vendor your Rust dependencies to avoid making network requests during the build phase, since reaching out to crates.io/github will be blocked by the opam sandbox. To do this you should run: + +```shell +cargo vendor +``` + +then follow the instructions for editing `.cargo/config` + + To simplify the full setup process, take a look at [ocaml-rust-starter](https://github.com/zshipko/ocaml-rust-starter). +## Build options + +By default, building `ocaml-sys` will invoke the `ocamlopt` command to figure out the version and location of the OCaml compiler. There are a few environment variables to control this. + +- `OCAMLOPT` (default: `ocamlopt`) is the command that will invoke `ocamlopt` +- `OCAML_VERSION` (default: result of `$OCAMLOPT -version`) is the target runtime OCaml version. +- `OCAML_WHERE_PATH` (default: result of `$OCAMLOPT -where`) is the path of the OCaml standard library. +- `OCAML_INTEROP_NO_CAML_STARTUP` (default: unset) can be set when loading an `ocaml-rs` library into an OCaml + bytecode runtime (such as `utop`) to avoid linking issues with `caml_startup` + +If both `OCAML_VERSION` and `OCAML_WHERE_PATH` are present, their values are used without invoking `ocamlopt`. If any of those two env variables is undefined, then `ocamlopt` will be invoked to obtain both values. + +Defining the `OCAML_VERSION` and `OCAML_WHERE_PATH` variables is useful for saving time in CI environments where an OCaml install is not really required (to run `clippy` for example). + +### Features + +- `derive` + * enabled by default, adds `#[ocaml::func]` and friends and `derive` implementations for `FromValue` and `ToValue` +- `link` + * link the native OCaml runtime, this should only be used when no OCaml code will be linked statically +- `no-std` + * Allows `ocaml` to be used in `#![no_std]` environments like MirageOS + + ## Writing your first `ocaml::func` `ocaml::func` is the highest-level macro that can be used to generate OCaml functions. It's built on `ocaml::native_func` which only works on `Value` parameters and `ocaml::bytecode_func` which is used for generating bytecode functions. `ocaml::func` will take care of generating bytecode bindings for functions with more than five parameters as required by the OCaml runtime. `ocaml::func` handles using `CAMLparam`/`CAMLlocal`/`CAMLreturn` correctly for you, often making it much easier to write bindings than using the C API directly, particularly for those who haven't used the OCaml C API before. diff --git a/doc/src/02_type_conversion.md b/doc/src/02_type_conversion.md index 9eba1df5..f9d4e89c 100644 --- a/doc/src/02_type_conversion.md +++ b/doc/src/02_type_conversion.md @@ -31,6 +31,9 @@ Below is a list of types that implement these traits in `ocaml-rs` and their cor | `BTreeMap` | `('a, 'b) list` | | `LinkedList` | `'a list` | +NOTE: Even though `&[Value]` is specifically marked as no copy, any type like `Option` would also qualify since the inner value is not converted to a Rust type. However, `Option` will do full unmarshaling into Rust types. Another thing to note: `FromValue` for `str` and `&[u8]` is zero-copy, however `ToValue` for `str` and `&[u8]` creates a new value - this is necessary to ensure the string is registered with the OCaml runtime. + +If you're concerned with minimizing allocations/conversions you should use `Value` type directly. ## Implementing `ToValue` and `FromValue` @@ -63,6 +66,8 @@ This can also be accomplished using the derive macros: pub struct MyType(i32); ``` +`derive(ToValue, FromValue)` will work on any struct or enum that are comprised of types that also implement `ToValue` and `FromValue` + ## Types that work directly on OCaml values There are several types that work directly on OCaml values, these don't perform any copies when converting to and from `Value`. diff --git a/doc/src/03_writing_ocaml_functions_in_rust.md b/doc/src/03_writing_ocaml_functions_in_rust.md index 8d5ab6a9..6c9fb8ba 100644 --- a/doc/src/03_writing_ocaml_functions_in_rust.md +++ b/doc/src/03_writing_ocaml_functions_in_rust.md @@ -1,6 +1,15 @@ # Writing OCaml functions in Rust -This section requires the `derive` feature, which is enabled in `ocaml-rs` by default. This exposes `ocaml::func`, which is the recommended way to create an OCaml function in Rust. +This section requires the `derive` feature, which is enabled in `ocaml-rs` by default. This exposes `ocaml::func`, which is the recommended way to create an OCaml function in Rust. Below are some examples using `ocaml::func` + +- [Hello world](#hello-world) +- [Structs and enums](#structs-and-enums) +- [Calling an OCaml function](#calling-an-ocaml-function) +- [Abstract types](#abstract-types) +- [Raising an exception](#raising-an-exception) +- [Returning OCaml result](#returning-ocaml-resut) +- [Using `Value` directly](#using-value-directly) +- [Unboxed arguments](#unboxed-arguments) ## Hello world @@ -15,7 +24,7 @@ pub fn hello_world() -> &'static str { } ``` -## `derive(ToValue)` and `derive(FromValue)` +## Structs and enums The example uses `derive(ToValue)` and `derive(FromValue)` to create an enum and struct that can be used as parameters to `ocaml::func`s @@ -32,7 +41,7 @@ pub enum BinOp { } #[derive(ocaml::FromValue, ocaml::ToValue)] -#[ocaml::sig("{lhs: float; rhs: float; op: bin_op}")] +#[ocaml::sig("lhs: float; rhs: float; op: bin_op")] pub struct Expr { lhs: f64, rhs: f64, @@ -52,6 +61,34 @@ pub fn expr_eval(expr: Expr) -> f64 { ``` +## Calling an OCaml function + +This example shows how to call an OCaml function from Rust - the OCaml function must be registered using `Callback.register`. In this case we're calling the OCaml function `my_incr`, which looks like this: + +```ocaml +let my_incr x = x + 1 +let () = Callback.register "my_incr" my_incr +``` + +```rust +# extern crate ocaml; + +ocaml::import! { + fn my_incr(x: ocaml::Int) -> ocaml::Int; +} + +#[ocaml::func] +#[ocaml::sig("int -> int")] +pub unsafe fn call_my_incr(x: ocaml::Int) -> Result { + my_incr(gc, &x) +} +``` + +A few things to note: + +- When calling the `import!`ed function you will need to pass the OCaml runtime handle as the first parameter +- The return value of the function will be wrapped in `Result` because the function may raise an exception + ## Abstract types This example shows how to wrap a Rust type using `ocaml::Pointer` @@ -87,6 +124,62 @@ pub unsafe fn file_close(file: ocaml::Pointer) { } ``` +## Raising an exception + +Raising an exception is accomplished by panicking: + + +```rust +# extern crate ocaml; + +#[ocaml::func] +#[ocaml::sig("int -> unit")] +pub unsafe fn fail_if_even_panic(i: ocaml::Int) { + if i % 2 == 0 { + panic!("even") + } +} +``` + +or returning a `Result<_, ocaml::Error>` value: + + +```rust +# extern crate ocaml; + +#[ocaml::func] +#[ocaml::sig("int -> unit")] +pub unsafe fn fail_if_even_result(i: ocaml::Int) -> Result<(), ocaml::Error> { + if i % 2 == 0 { + return Err(ocaml::Error::Caml(ocaml::CamlError::Failure("even"))) + } + + Ok(()) +} +``` + +## Returning OCaml result + +In the previous example `Result<_, ocaml::Error>` was used to raise an exception, however `Result` where `A` and `B` both implement `ToValue` will create an OCaml `('a, 'b) Result.t`: + +```rust +# extern crate ocaml; +use ocaml::{ToValue}; + +#[ocaml::func] +#[ocaml::sig("string -> (int, [`Msg of string]) result")] +pub unsafe fn try_int_of_string(s: &str) -> Result { + match s.parse::() { + Ok(i) => Ok(i), + Err(e) => { + let s = format!("{e:?}"); + let err = ocaml::Value::hash_variant(gc, "Msg", Some(s.to_value(gc))); + Err(err) + } + } +} +``` + ## Using `Value` directly It is also possible to use `ocaml::Value` to avoid any conversion or copying, however this can be more error prone. @@ -101,4 +194,26 @@ pub unsafe fn array_set(mut array: ocaml::Value, index: ocaml::Value, s: ocaml:: } ``` +## Unboxed arguments + +Unfortunately `ocaml::func` doesn't support unboxed/noalloc functions, however it is still possible to create them using `ocaml-rs`: + +```rust +# extern crate ocaml; + +#[no_mangle] +pub extern "C" fn unboxed_float_avg(a: f64, b: f64) -> f64 { + (a + b) / 2.0 +} + +#[ocaml::bytecode_func] +pub fn unboxed_float_avg_bytecode(a: f64, b: f64) -> f64 { + unboxed_float_avg(a, b) +} +``` + +In this case you will also need to write the signature manually: +```ocaml +external unboxed_float_avg: float -> float -> float = "unboxed_float_avg_bytecode" "unboxed_float_avg" [@@unboxed] [@@noalloc] +``` diff --git a/src/lib.rs b/src/lib.rs index 61c0f28e..20dea9d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,6 +118,8 @@ //! ``` //! //! Excluding the `incrf` example, these can also be automatically generated using [ocaml-build](https://github.com/zshipko/ocaml-rs/blob/master/build/README.md) +//! +//! For more information see the [ocaml-rs book](https://zshipko.github.io/ocaml-rs) #[cfg(all(feature = "link", feature = "no-std"))] std::compile_error!("Cannot use link and no-std features");