Allow WASM shaper code to access features (#6139)

* Add WASM API to wrap features

* Access features through WASM FFI
diff --git a/src/hb-wasm-api-list.hh b/src/hb-wasm-api-list.hh
index 5db45c7..62e3bc6 100644
--- a/src/hb-wasm-api-list.hh
+++ b/src/hb-wasm-api-list.hh
@@ -92,6 +92,8 @@
 
   /* shape */
   NATIVE_SYMBOL ("(iiii$)i",	shape_with),
+  NATIVE_SYMBOL ("(iii)i",	feature_copy_contents),
+  NATIVE_SYMBOL ("(i)",		features_free),
 
   /* debug */
 #ifdef HB_DEBUG_WASM
diff --git a/src/hb-wasm-api-shape.hh b/src/hb-wasm-api-shape.hh
index 622ff05..7bd45d0 100644
--- a/src/hb-wasm-api-shape.hh
+++ b/src/hb-wasm-api-shape.hh
@@ -64,6 +64,52 @@
 			shaper_list);
 }
 
+HB_WASM_API (bool_t, feature_copy_contents) (HB_WASM_EXEC_ENV
+					       ptr_d(const feature_t, features),
+					       uint32_t num_features,
+					       ptr_d(features_t, output))
+{
+  HB_PTR_PARAM (features_t, output);
+  if (unlikely (!output))
+    return false;
+
+  unsigned bytes;
+  if (unlikely (hb_unsigned_mul_overflows (num_features, sizeof (feature_t), &bytes)))
+    return false;
+
+  if (!num_features)
+  {
+    output->length = 0;
+    output->features = nullref;
+    return true;
+  }
+
+  HB_ARRAY_PARAM (const feature_t, features, num_features);
+  if (unlikely (!features))
+    return false;
+
+  feature_t *dst = nullptr;
+  uint32_t dstref = module_malloc (bytes, (void **) &dst);
+  if (unlikely (!dstref))
+    return false;
+
+  hb_memcpy (dst, features, bytes);
+  output->length = num_features;
+  output->features = dstref;
+  return true;
+}
+
+HB_WASM_API (void, features_free) (HB_WASM_EXEC_ENV
+				    ptr_d(features_t, features))
+{
+  HB_PTR_PARAM (features_t, features);
+  if (unlikely (!features))
+    return;
+
+  module_free (features->features);
+  features->features = nullref;
+  features->length = 0;
+}
 
 }}
 
diff --git a/src/hb-wasm-api.h b/src/hb-wasm-api.h
index bb977ee..71afea2 100644
--- a/src/hb-wasm-api.h
+++ b/src/hb-wasm-api.h
@@ -297,6 +297,21 @@
 #define FEATURE_GLOBAL_START	0
 #define FEATURE_GLOBAL_END	((uint32_t) -1)
 
+typedef struct
+{
+  uint32_t length;
+  ptr_t(feature_t) features;
+} features_t;
+#define FEATURES_INIT {0, 0}
+
+HB_WASM_API (bool_t, feature_copy_contents) (HB_WASM_EXEC_ENV
+					       ptr_d(const feature_t, features),
+					       uint32_t num_features,
+					       ptr_d(features_t, output));
+
+HB_WASM_API (void, features_free) (HB_WASM_EXEC_ENV
+				     ptr_d(features_t, features));
+
 HB_WASM_API (bool_t, shape_with) (HB_WASM_EXEC_ENV
 				  ptr_d(font_t, font),
 				  ptr_d(buffer_t, buffer),
diff --git a/src/wasm/rust/harfbuzz-wasm/src/lib.rs b/src/wasm/rust/harfbuzz-wasm/src/lib.rs
index ea20ce5..5610b5a 100644
--- a/src/wasm/rust/harfbuzz-wasm/src/lib.rs
+++ b/src/wasm/rust/harfbuzz-wasm/src/lib.rs
@@ -45,6 +45,8 @@
     fn face_copy_table(font: u32, tag: u32, blob: *mut Blob) -> bool;
     fn buffer_copy_contents(buffer: u32, cbuffer: *mut CBufferContents) -> bool;
     fn buffer_set_contents(buffer: u32, cbuffer: &CBufferContents) -> bool;
+    fn feature_copy_contents(features: u32, num_features: u32, output: *mut CFFeatures) -> bool;
+    fn features_free(features: *mut CFFeatures);
     fn debugprint(s: *const u8);
     fn shape_with(
         font: u32,
@@ -363,6 +365,24 @@
     pub height: i32,
 }
 
+/// An OpenType feature passed to the shaper.
+#[derive(Debug, Clone, Copy)]
+pub struct Feature {
+    /// The feature tag (e.g. `TAG(b'k', b'e', b'r', b'n')`)
+    pub tag: [u8; 4],
+    /// The feature value (0 = off, 1 = on, etc.)
+    pub value: u32,
+    /// Start index of the feature range, or `FEATURE_GLOBAL_START`
+    pub start: u32,
+    /// End index of the feature range, or `FEATURE_GLOBAL_END`
+    pub end: u32,
+}
+
+/// Start index for features that apply globally.
+pub const FEATURE_GLOBAL_START: u32 = 0;
+/// End index for features that apply globally.
+pub const FEATURE_GLOBAL_END: u32 = u32::MAX;
+
 #[derive(Debug)]
 #[repr(C)]
 struct CBufferContents {
@@ -371,6 +391,22 @@
     position: *mut CGlyphPosition,
 }
 
+#[derive(Debug)]
+#[repr(C)]
+struct CFFeatures {
+    length: u32,
+    features: *mut CFeature,
+}
+
+#[derive(Debug, Clone, Copy)]
+#[repr(C)]
+struct CFeature {
+    tag: u32,
+    value: u32,
+    start: u32,
+    end: u32,
+}
+
 /// Ergonomic representation of a Harfbuzz buffer item
 ///
 /// Harfbuzz buffers are normally split into two arrays,
@@ -455,6 +491,78 @@
 /// Our default buffer item struct. See also [`Glyph`].
 pub type GlyphBuffer = Buffer<Glyph>;
 
+/// A collection of OpenType features passed to the shaper.
+#[derive(Debug)]
+pub struct Features {
+    features: Vec<Feature>,
+}
+
+fn u32_to_tag(tag: u32) -> [u8; 4] {
+    [
+        ((tag >> 24) & 0xFF) as u8,
+        ((tag >> 16) & 0xFF) as u8,
+        ((tag >> 8) & 0xFF) as u8,
+        (tag & 0xFF) as u8,
+    ]
+}
+
+impl Features {
+    /// Construct a `Features` from the raw pointer and count
+    /// passed to the `shape` function by the WASM host.
+    pub fn from_raw(ptr: u32, count: u32) -> Self {
+        let mut c_features = CFFeatures {
+            length: 0,
+            features: std::ptr::null_mut(),
+        };
+        if count > 0 {
+            unsafe {
+                feature_copy_contents(ptr, count, &mut c_features);
+            }
+        }
+        let slice =
+            unsafe { std::slice::from_raw_parts(c_features.features, c_features.length as usize) };
+        let features: Vec<Feature> = slice
+            .iter()
+            .map(|f| Feature {
+                tag: u32_to_tag(f.tag),
+                value: f.value,
+                start: f.start,
+                end: f.end,
+            })
+            .collect();
+        Features { features }
+    }
+
+    /// Returns the number of features.
+    pub fn len(&self) -> usize {
+        self.features.len()
+    }
+
+    /// Returns true if there are no features.
+    pub fn is_empty(&self) -> bool {
+        self.features.is_empty()
+    }
+
+    /// Returns a slice of all features.
+    pub fn as_slice(&self) -> &[Feature] {
+        &self.features
+    }
+
+    /// Iterate over the features.
+    pub fn iter(&self) -> impl Iterator<Item = &Feature> {
+        self.features.iter()
+    }
+}
+
+impl Drop for Features {
+    fn drop(&mut self) {
+        // CFFeatures is stack-allocated but it points to WASM memory.
+        // We don't need to free since feature_copy_contents allocates
+        // in WASM heap and we copied values out. The WASM module
+        // frees its heap on deinstantiation.
+    }
+}
+
 /// Write a string to the Harfbuzz debug log.
 pub fn debug(s: &str) {
     let c_s = CString::new(s).unwrap();