[wasm] Improve Rust docs, refer to new crate

This commit is contained in:
Simon Cozens 2023-02-25 15:20:39 +00:00 committed by Behdad Esfahbod
parent 28a7c1f932
commit 840b5dff73
1 changed files with 71 additions and 36 deletions

View File

@ -4,37 +4,39 @@ If the standard OpenType shaping engine doesn't give you enough flexibility, Har
## How to write a shaping engine in Rust ## How to write a shaping engine in Rust
Here are the steps to create an example shaping engine in Rust: Here are the steps to create an example shaping engine in Rust: (These examples can also be found in `src/wasm/sample/rust`)
* First, install wasm-pack, which helps us to generate optimized WASM files. It writes some Javascript bridge code that we don't need, but it makes the build and deployment process much easier: * First, install wasm-pack, which helps us to generate optimized WASM files. It writes some Javascript bridge code that we don't need, but it makes the build and deployment process much easier:
``` ```
cargo install wasm-pack $ cargo install wasm-pack
``` ```
* Now let's create a new library: * Now let's create a new library:
``` ```
cargo new --lib my-shaper $ cargo new --lib hello-wasm
``` ```
* We need the target to be a dynamic library, and we're going to use `bindgen` to export our Rust function to WASM, so let's put these lines in the `Cargo.toml`: * We need the target to be a dynamic library, and we're going to use `bindgen` to export our Rust function to WASM, so let's put these lines in the `Cargo.toml`. The Harfbuzz sources contain a Rust crate which makes it easy to create the shaper, so we'll specify that as a dependency as well:
```toml ```toml
[lib] [lib]
crate-type = ["cdylib"] crate-type = ["cdylib"]
[dependencies] [dependencies]
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
harfbuzz-wasm = { path = "your-harfbuzz-source/src/wasm/rust/harfbuzz-wasm"}
``` ```
*
* And now we'll create our shaper code. In `src/lib.rs`: * And now we'll create our shaper code. In `src/lib.rs`:
``` ```rust
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[wasm_bindgen] #[wasm_bindgen]
pub fn shape(_font_ref: u32, _buf_ref: u32) -> i32 { pub fn shape(_font_ref: u32, _buf_ref: u32, _features: u32, _num_features: u32) -> i32 {
1 1 // success!
} }
``` ```
@ -45,7 +47,7 @@ This exports a shaping function which takes two arguments, tokens representing t
``` ```
INFO]: 🎯 Checking for the Wasm target... INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm... [INFO]: 🌀 Compiling to Wasm...
Compiling my-shaper v0.1.0 (...) Compiling hello-wasm v0.1.0 (...)
Finished release [optimized] target(s) in 0.20s Finished release [optimized] target(s) in 0.20s
[WARN]: ⚠️ origin crate has no README [WARN]: ⚠️ origin crate has no README
[INFO]: ⬇️ Installing wasm-bindgen... [INFO]: ⬇️ Installing wasm-bindgen...
@ -54,14 +56,14 @@ INFO]: 🎯 Checking for the Wasm target...
[INFO]: ✨ Done in 0.40s [INFO]: ✨ Done in 0.40s
``` ```
You'll find the output WASM file in `pkg/my_shaper_bg.wasm` You'll find the output WASM file in `pkg/hello_wasm_bg.wasm`
* Now we need to get it into a font. * Now we need to get it into a font.
One way to do this is to use [otfsurgeon](https://github.com/simoncozens/font-engineering/blob/master/otfsurgeon), a tool for manipulating binary font tables: We provide a utility to do this called `addTable.py` in the `src/` directory:
``` ```
% otfsurgeon -i test.ttf add -o test-wasm.ttf Wasm < pkg/my_shaper_bg.wasm % python3 ~/harfbuzz/src/addTable.py test.ttf test-wasm.ttf pkg/hello_wasm_bg.wasm
``` ```
And now we can run it! And now we can run it!
@ -77,41 +79,74 @@ Congratulations! Our shaper did nothing, but in Rust! Now let's do something - i
* To say hello world, we're going to have to use a native function. * To say hello world, we're going to have to use a native function.
In debugging builds of Harfbuzz, we can print some output from the web assembly module to the host's standard output using the `debugprint` function: In debugging builds of Harfbuzz, we can print some output from the web assembly module to the host's standard output using the `debug` function. To make this easier, we've got the `harfbuzz-wasm` crate:
``` ```rust
// We don't use #[wasm_bindgen] here because that makes use harfbuzz_wasm::debug;
// assumptions about Javascript calling conventions. We
// really do just want to import some C symbols and run
// them in unsafe-land!
extern "C" {
pub fn debugprint(s: *const u8);
}
```
And now let's add a function on the Rust side which makes this a bit more ergonomic to use:
```
use std::ffi::CString;
fn print(s: &str) {
let c_s = CString::new(s).unwrap();
unsafe {
debugprint(c_s.as_ptr() as *const u8);
};
}
#[wasm_bindgen] #[wasm_bindgen]
pub fn shape(font_ref: i32, buf_ref: i32) -> i32 { pub fn shape(_font_ref: u32, _buf_ref: u32, _features: u32, _num_features: u32) -> i32 {
print("Hello from Rust!\n"); debug("Hello from Rust!\n");
1 1
} }
``` ```
With this compiled into a WASM module, and installed into our font again, finally our fonts can talk to us!s With this compiled into a WASM module, and installed into our font again, finally our fonts can talk to us!
``` ```
$ hb-shape test-wasm.ttf abc $ hb-shape test-wasm.ttf abc
Hello from Rust! Hello from Rust!
[cent=0|sterling=1|fraction=2] [cent=0|sterling=1|fraction=2]
``` ```
Now let's start to do some actual, you know, *shaping*. The first thing a shaping engine normally does is (a) map the items in the buffer from Unicode codepoints into glyphs in the font, and (b) set the advance width of the buffer items to the default advance width for those glyphs. We're going to need to interrogate the font for this information, and write back to the buffer. Harfbuzz provides us with opaque pointers to the memory for the font and buffer, but we can turn those into useful Rust structures using the `harfbuzz-wasm` crate again:
```rust
use wasm_bindgen::prelude::*;
use harfbuzz_wasm::{Font, GlyphBuffer};
#[wasm_bindgen]
pub fn shape(_font_ref: u32, _buf_ref: u32, _features: u32, _num_features: u32) -> i32 {
let font = Font::from_ref(font_ref);
let mut buffer = GlyphBuffer::from_ref(buf_ref);
for mut item in buffer.glyphs.iter_mut() {
// Map character to glyph
item.codepoint = font.get_glyph(codepoint, 0);
// Set advance width
item.h_advance = font.get_glyph_h_advance(item.codepoint);
}
1
}
```
The `GlyphBuffer`, unlike in Harfbuzz, combines positioning and information in a single structure, to save you having to zip and unzip all the time. It also takes care of marshalling the buffer back to Harfbuzz-land; when a GlyphBuffer is dropped, it writes its contents back through the reference into Harfbuzz's address space. (If you want a different representation of buffer items, you can have one: `GlyphBuffer` is implemented as a `Buffer<Glyph>`, and if you make your own struct which implements the `BufferItem` trait, you can make a buffer out of that instead.)
One easy way to write your own shapers is to make use of OpenType shaping for the majority of your shaping work, and then make changes to the pre-shaped buffer afterwards. You can do this using the `Font.shape_with` method. Run this on a buffer reference, and then construct your `GlyphBuffer` object afterwards:
```rust
use harfbuzz_wasm::{Font, GlyphBuffer};
use tiny_rng::{Rand, Rng};
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn shape(font_ref: u32, buf_ref: u32) -> i32 {
let mut rng = Rng::from_seed(123456);
// Use the default OpenType shaper
let font = Font::from_ref(font_ref);
font.shape_with(buf_ref, "ot");
// Now we have a buffer with glyph ids, advance widths etc.
// already filled in.
let mut buffer = GlyphBuffer::from_ref(buf_ref);
for mut item in buffer.glyphs.iter_mut() {
// Randomize it!
item.x_offset = ((rng.rand_u32() as i32) >> 24) - 120;
item.y_offset = ((rng.rand_u32() as i32) >> 24) - 120;
}
1
}
```
See the documentation for the `harfbuzz-wasm` crate for all the other