flense 0.2.1

Purpose-oriented lensing
Documentation

flense

Purpose-oriented lensing for Rust.

State

This is not production ready and should not be relied on. The tests pass under miri, but no thorough deep validation of the pointers and logic has been performed that would give a high degree of confidence. Stacked borrows are hard.

High-level

Rather than focusing on accessing specific fields of specific structures, flense allows a sort of duck-typing for lenses, where you annotate purpose-oriented fields like Position or Color (or whatever you please!), then define how to adapt the data inside your structure into flense.

Other lensing libraries can only lense the original structure and its original fields. You can still achieve that with flense by creating a specialized Field for each specific structure field.

Invalid mutable lenses which alias data should be prevented at compile time, though cargo check may fail to find them when cargo build does.

Properties

Creating a lens is simple: some_data.lens(). Type inference drives the creation of the correct lens. A future goal is the creation of a derive macro which can help remove the need to manually specify adapter implementations, though they will always be possible to implement on any type.

This crate should largely compile out and be zero overhead. Functions taking reified lenses have a small performance overhead since the size of a lens is dependent on the number of fields in it.

Examples

use flense::prelude::*;

// Some common fields you might wish to use. Maybe you're creating lenses for
// different data structures that might store position in other locations inside
// themselves, or your library wants to access something that is semantically a
// position but to use user-provided data types in processing without requiring
// allocations and transformations back and forth.
enum Position {}
impl Field for Position { type Type = [f32; 3]; }
enum ColorRgb {}
impl Field for ColorRgb { type Type = [u8; 3]; }
enum Normal {}
impl Field for Normal { type Type = [f32; 2]; }

// A plain old data type, defined anywhere - if it was defined outside your lib,
// you could wrap it in a #[repr(transparent)] wrapper and pierce through it.
#[derive(Clone, Copy, Default)]
struct Vertex {
    pub position: [f32; 3],
    pub color_rgb: [u8; 3],
    pub normal: [f32; 2],
}

// Fundamentally, the safety constraint on `Adapter` is: you must provide the
// offset, in bytes, from the beginning of the structure to the adapted field,
// which must be the exact type that is defined in the `Field`.
// In the future, a derive macro might allow annotating structure fields with
// `Field` to derive the correct offset automatically.
unsafe impl Adapter<Position> for Vertex {
    const OFFSET: usize = std::mem::offset_of!(Self, position);
}
unsafe impl Adapter<ColorRgb> for Vertex {
    const OFFSET: usize = std::mem::offset_of!(Self, color_rgb);
}
unsafe impl Adapter<Normal> for Vertex {
    const OFFSET: usize = std::mem::offset_of!(Self, normal);
}

// You can also implement reflexive Adapters; a blanket implementation cannot be
// provided as it would prevent other implementations, since a Field implemented
// remotely could change its type.
// In the future, a derive macro might provide this functionality.
unsafe impl Adapter<Position> for <Position as Field>::Type {
    const OFFSET: usize = 0;
}

// Somewhere else, possibly even provided in a library which is unaware of the
// concrete Vertex definition entirely.
fn compute_bounds<'a>(positions: impl LensesSlice<'a, (Position,)>) -> [f32; 3] {
    [0.0, 0.0, 0.0] // todo!()
}

// Or, you can avoid performing code generation of the body multiple times by
// separating the lens construction from its usage, though this may prevent
// optimizations like vectorization.
#[inline]
fn compute_bounds_outer<'a>(positions: impl LensesSlice<'a, (Position,)>) -> [f32; 3] {
    compute_bounds_inner(positions.lens_slice())
}
fn compute_bounds_inner(positions: LensSlice<(Position,)>) -> [f32; 3] {
    // This inner function has no generic parameters whatsoever, but the outer
    // function adapts any structure that can be lensed to provide a position!
    // This lets you reduce binary size by avoiding monomorphization for every
    // type that has a `Position`.
    [0.0, 0.0, 0.0] // todo!()
}

fn main() {
    let some_vertices = vec![Vertex::default(); 100];
    compute_bounds(&*some_vertices);
    compute_bounds_outer(&*some_vertices);

    let some_contiguous_data = &[[0.0f32, 0.0, 0.0]; 100][..];
    compute_bounds(some_contiguous_data);
    compute_bounds_outer(some_contiguous_data);
}
use flense::prelude::*;

enum Position {}
impl Field for Position { type Type = [f32; 2]; }
enum Velocity {}
impl Field for Velocity { type Type = [f32; 2]; }

#[derive(Clone, Copy, Default)]
struct ParticleAos {
    pub position: [f32; 2],
    pub velocity: [f32; 2],
}

unsafe impl Adapter<Position> for ParticleAos {
    const OFFSET: usize = std::mem::offset_of!(Self, position);
}
unsafe impl Adapter<Velocity> for ParticleAos {
    const OFFSET: usize = std::mem::offset_of!(Self, velocity);
}

unsafe impl Adapter<Position> for <Position as Field>::Type {
    const OFFSET: usize = 0;
}
unsafe impl Adapter<Velocity> for <Velocity as Field>::Type {
    const OFFSET: usize = 0;
}

// hacky drag for the velocities via linear decay
fn approach_zero(value: f32, amount: f32) -> f32 {
    (value.abs() - amount).max(0.0) * value.signum()
}

// positions is guaranteed to be rectangular, so you don't have to worry about
// one slice being longer or shorter than the other
fn step_particles<'a>(dt: f32, particles: impl LensesSliceMut<'a, (Position, Velocity)>) {
    for lens in particles.lens_slice_mut() {
        let (mut position, mut velocity) = lens.split::<(Position,), _>();
        let position = position.as_mut::<Position, _>();
        let velocity = velocity.as_mut::<Velocity, _>();
        position[0] += velocity[0] * dt;
        position[1] += velocity[1] * dt;
        velocity[0] = approach_zero(velocity[0], dt);
        velocity[1] = approach_zero(velocity[1], dt);
    }
}

// or you can take multiple lenses for more flexibility
fn step_particles_multi<'a>(
    dt: f32,
    positions: impl LensesSliceMut<'a, (Position,)>,
    velocities: impl LensesSliceMut<'a, (Velocity,)>
) {
    let positions = positions.lens_slice_mut();
    let velocities = velocities.lens_slice_mut();
    for (mut position, mut velocity) in positions.into_iter().zip(velocities) {
        let position = position.as_mut::<Position, _>();
        let velocity = velocity.as_mut::<Velocity, _>();
        position[0] += velocity[0] * dt;
        position[1] += velocity[1] * dt;
        velocity[0] = approach_zero(velocity[0], dt);
        velocity[1] = approach_zero(velocity[1], dt);
    }
}

fn main() {
    let mut particles_aos = vec![
        ParticleAos { position: [0.0, 0.0], velocity: [2.0, 0.0] },
        ParticleAos { position: [0.0, 0.0], velocity: [0.0, 2.0] },
    ];

    // Use with an array of structures...
    step_particles(1.0, &mut particles_aos[..]);
    {
        let lens: LensSliceMut<'_, (Position, Velocity)> = particles_aos.lens_slice_mut();
        let (lhs, rhs) = lens.split::<_, _>();
        step_particles_multi(1.0, lhs, rhs);
    }
    assert_eq!(particles_aos[0].position, [3.0, 0.0]);
    assert_eq!(particles_aos[1].position, [0.0, 3.0]);

    let mut soa_positions = vec![[0.0, 0.0]; 2];
    let mut soa_velocities = vec![[2.0, 0.0], [0.0, 2.0]];

    // Or with a structure of arrays! quality of life is a work in progress
    step_particles(
        1.0,
        // form one lens by joining two separate ones - these could live in
        // entirely different data structures or parts of memory
        <&mut _ as flense::lenses::LensesSliceMut<'_, (Position,)>>::lens_slice_mut(&mut soa_positions)
            .join::<(Velocity,)>(soa_velocities.lens_slice_mut()).unwrap()
    );
    step_particles_multi(
        1.0,
        &mut soa_positions[..],
        &mut soa_velocities[..],
    );
    assert_eq!(soa_positions[0], [3.0, 0.0]);
    assert_eq!(soa_positions[1], [0.0, 3.0]);
}

Prior art, other lenses

flense was heavily inspired by some work I've done in creating a sound wrapper for meshoptimizer (which will eventually be published). In doing so, I explored concepts used in terrors and frunk.

  • grist_lens is another lens library which I saw the announcement of a few days before separating this one from my meshoptimizer wrapper, though I haven't used it. I did, however, read the author's blog post going over the various lens options she has seen and used.
  • lens-rs
  • pl-lens