How to make an ergonomic wrapper library API for (x,y) Point type for C FFI


#1

I’m a rust newbie making a wrapper crate for a small C library that uses geometric 2D points. To the C lib, the points are passed as pointers to length-2 arrays [x, y]. The data is not mutated by the c library.

// my library:
mod ffi {
   #[link(name = "thelib")]
   extern "C" { pub fn c_function(p1: *const f64, p2: *const f64) -> f64; }
}

What is a way of writing the API for this, in a way that lets the user re-use his own Point type, and is still performant and idiomatic (I believe this is a case where I have to pick two…)
A caller wouldn’t want to take his 10000 points and create an array of 10000 with a type from my library, if it’s avoidable (future additions to my API will include methods that take long arrays of data).

For my first attempt, I simply made a struct in my library with [repr(C)] and passed a reference to the c method, e.g.

// my library:
[repr(C)]
struct Point { x: f64, y: f64 }

impl Point {
   pub fn wrapper_method(&self, p: &Self) -> f64 {
       unsafe { ffi::c_function(&p.x, &self.x) }
   }
}

This works, but isn’t very ergonomic: I’m imposing my Point type on the caller.
The scenario I’m trying to solve is that different calling code will have different point types, but they will normally be similar to one of these:

// calling code:
struct ExamplePointStruct { x: f64, y:f64 }
// or
struct ExamplePointTuple(f64, f64);

So for my next attempt I’m trying this: I make a trait and a struct

// my library:
#[derive(Debug)]
#[repr(C)]
struct Coords { x: f64, y: f64 }

trait Point: Sized {
    fn coords(&self) -> &Coords;
    fn wrapper_method(&self, p: &Self) -> f64 {
        let c1 = p.coords();
        let c2 = self.coords();
        unsafe { ffi::c_function(&c1.x, &c2.x) }
    }    
}

Theoretically, the calling code should now have the option to create the Coords struct to pass my library, or if they are concious about performance it should be possible to just fool the compiler that their two-floats-in-a-row are in fact the coords, through some voodoo such as

// calling code:
#[repr(C)]
struct ExamplePointStruct { x: f64, y: f64 }

// Safe/slower:
impl Point for ExamplePointStruct {
   fn coords(&self) -> &Coords {
      &Coords{ x: self.x, y: self.y }
   }
}

// Unsafe/faster:
impl Point for ExamplePointStruct {
   fn coords(&self) -> &Coords {
      unsafe { mem::transmute::<&ExamplePointStruct, &Coords>(&self) } // ??
   }
}

Does any of this make sense at all? Is this a very peculiar special case I’m doing (trying to accomodate the callers’ unknown type, for which I think I can guess the layout only)? Does the trait-and-type thing look reasonable? Are there other more idiomatic ways to do this e.g. with some conversion trait?

Thanks for any pointers or references…


#2

Copying a pair of 64-bit floats is not much more expensive than copying a pointer, especially on a 64-bit CPU, so the “safe/slower” version of the code might actually be fast enough. But if you do find that pointer casting is necessary for good performance here’s one approach you could use:

#[repr(C)]
struct Point { x: f64, y: f64 }

unsafe trait AsPoint {
    fn as_point(&self) -> &Point {
        unsafe {
            &*(self as *const Self as *const Point)
        }
    }
}

#[repr(C)]
struct OtherPoint(f64, f64);
unsafe impl AsPoint for OtherPoint {}

fn do_stuff<P: AsPoint>(point: &P) {
    let p = point.as_point();
    // ...
}

#3

Thanks, that looks better. I realize copying 2x64 bits is normally fast, but I’m anticipating the library will eventually have functions that takes lists of coordinates (e.g. polygons, point clouds), and here is where I’m hoping to be able to avoid allocating new arrays. Is the “as_something_else” pattern possible to extend to this scenario?

I suppose in the end it’s still not very ergonomic if my library returns lists of my own types rather than whatever type the user had.

Example: say my library does polygon intersections (it doesn’t at least not yet…) which returns a new polygon

The client code has these types

#[repr(C)]
struct OtherPoint(f64, f64);
type Polygon<'a> = &'a [OtherPoint];

let p1 = large_poly(); // say 10000 points
let p2 = another_large_poly(); // say another 10000 points

What I’d prefer is for my library to do magic and achieve this

let p3 = my_library::intersect_polygon(&p1, &p2); // No copying, same return type

Is this even remotely possible?


#4

Here’s a generalization of the unsafe trait approach, making the trait generic: https://is.gd/z1L5hf

Once the trait is generic, you can also provide generic implementations for slices, arrays, tuples, etc. For example:

unsafe impl<'a, T, U> SameLayout<&'a [U]> for &'a [T] where T: SameLayout<U> {}

Then if OtherPoint implements SameLayout<Point>, the conversion from &[OtherPoint] to &[Point] is implemented automatically (and still without copying any data).


#5

That’s some impressive type voodoo, thanks.