googlefonts / googlefonts/oxidize
A hybrid, safe approach
- Dominant language
- No language data
- Stars
- 351
- Forks
- 13
- PR merge metrics
- No merged PRs in 30d
Description
### the important question part
*How much, exactly, do we dislike copying*? Below I am going to outline an approach that copies/converts *scalar* fields greedily, as objects are accessed. With this approach, we would still avoid a *lot* of copying (we would only ever copy bytes on tables that we accessed, and there still quite minimally) and in exchange for spending a few cycles up front, we end up with a much more natural API.
### the explanation part
So far, we have been discussing two different take on a zerocopy API: first is the HarfBuzz model, where we take some chunk of bytes and directly reinterpret them as some type `T` that has fields we can access; and second is the pinot model, where instead of a struct with fields we have a 'view' type that generates methods for each field, and those methods reach into the bytes and interpret them as a given scalar value.
I like different parts of each of these approaches. For the HarfBuzz approach, I like that we end up with a struct that acts like any other (modulo the fact that you can't directly read the fields; you need to call getters) and which also is a more-or-less literal transcription of the items in the spec. For the pinot version, I like that there is no `transmute` (which is more heavily restricted in Rust than in cpp, as I understand it) and perhaps also that it lets the overall API (perhaps) be more consistent, because some things will probably make more sense as methods anyway.
I've been thinking about an approach I'll call 'copy leaves'. In this approach, in a given object, we distinguish between fields that are scalars and those that are collections/references. We always copy scalars, and we always create views into references.
In this world, a declaration of fvar would look something like,
```rust
#[derive(FontThing)]
pub struct Fvar<'a> {
blob: Blob<'a>,
// header
pub major_version: u16,
pub minor_version: u16,
pub axes_offset: Offset16,
reserved: u16,
axis_count: u16,
axis_size: u16,
pub instance_count: u16,
pub instance_size: u16,
// in general you can specify with annotations where the data should come from
#[font_thing(count(axis_count), offset(axes_offset))]
pub axes: Array<'a, VariationAxisRecord>,
// in fancy cases you can throw up your hands and just name arguments to
// be passed in at construction time?
#[font_thing(args(instance_count, axis_count, instance_size))]
pub instances: FvarInstancesArray<'a>,
}
```
maybe this wasn't the best example; I'll include a bit more code below, but I mainly want to highlight the basics:
- `axes` and `instances` are 'array' types; they wrap a blob of data, and provide an array-like API for random access. Most of the time, there can be shared types to do this (the `Array`, type, here) but in the specific case of the instances field on fvar we bail and use a custom type; we specify in the macro some arguments that should be passed to that type's constructor.
- when a type is instantiated, any scalars that are fields of that type are copied & converted
- then they act just like normal fields, and are native rust data types.
- alternatively, we could generate methods instead of fields for the non-scalars, which would be easier in some ways.
Is this a non-starter?
------
For the interested, this is just me sketching out how this might work implementation-wise:
```rust
/// Variation axis record.
#[derive(FontThing)]
pub struct VariationAxisRecord {
// ... omitted for brevity
}
// no derive, because we construct this by hand?
pub struct InstanceRecord<'a> {
pub subfamily_name_id: u16,
pub flags: u16,
pub coordinates: Array<'a, Fixed>,
pub postscript_name_id: Option,
}
// no derive, we construct this by hand
pub struct FvarInstancesArray<'a> {
blob: Blob<'a>,
count: u16,
instance_size: u16,
axis_count: u16,
}
impl<'a> FvarInstancesArray<'a> {
fn get(&self, ix: usize) -> Option> {
let start = ix * self.instance_size as usize;
let subfamily_name_id = blob.read(start)?;
let flags = blob.read(start + 2)?;
let coords_len = Fixed::SIZE * (self.axis_count as usize);
let coords_blob = blob.get(start + 4..start + 4 + coords_len)?;
let coordinates = Array::new(coords_blob);
let has_ps_name = instance_size - (axis_count * Fixed::SIZE) == 6;
let postscript_name_id: Option = if has_ps_name {
blob.read(start + 4 + coords_len)
} else {
None
};
InstanceRecord {
subfamily_name_id,
flags,
coordinates,
postscript_name_id
}
}
}
```
Contributor guide
Assessment
This issue has not been assessed yet.