Skip to content

Instantly share code, notes, and snippets.

@ChunMinChang
Last active October 18, 2021 07:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ChunMinChang/beb8db168260166e8f4759f97c7a11c7 to your computer and use it in GitHub Desktop.
Save ChunMinChang/beb8db168260166e8f4759f97c7a11c7 to your computer and use it in GitHub Desktop.
opaque or transparent interface of the external library

Opaque or Transparent Data Type in a Rust Library

To develop a Rust library that will be used for the external code, the data may be interoperable or non-interoperable. The interoperable data is a data whose underlying memory layout is known and their values can be modified directly by the external code. On the contrary, the non-interoperable data is a data whose underlying memory layout is unknown so their values cannot be changed directly. The underlying values can only be changed when they provide related APIs to do that.

In summary, an interoperable data must be a transparent-type data so it can be modified by external code, while a non-interoperable data must be an opaque-type data to prevent its values from being changed by the external code directly.

In this article, we will explain the above ideas in detail by using Rust-to-C examples.

Transparent Data Type in Rust

By marking a data type with #[repr(C)], the data is interoperable with C/C++ since its memory layout is guaranteed to be aligned with C. It can be called as a normal C data. For example, we can operate the data with type struct Foo from Rust

#[repr(C)]
struct Foo {
    bar: i32,
    baz: f32,
    qux: [u32; 5],
}

...

#[no_mangle]
pub extern "C" fn get_foo() -> *mut Foo {
    Box::into_raw(Box::new(Foo::new()))
}

...

in C code like:

typedef struct {
  int32_t bar;
  float baz;
  uint32_t qux[5];
} Foo;

...

Foo* foo = get_foo();
foo->qux[1] = 100; // Data with type `Foo` can be modified directly!

...

Opaque Data Type in Rust

For a non-interoperable data, we can just pass a pointer to its memory address with type *void. Without the knowledge of the underlaying data's memory layout, C/C++ code cannot do anything.

// No `#[repr(C)]` here!
struct Foo {
    bar: i32,
    baz: f32,
    qux: [u32; 5],
}

...

#[no_mangle]
pub extern "C" fn get_foo() -> *mut c_void {
    Box::into_raw(Box::new(Foo::new())) as *mut _
}

...
...

void* foo = get_foo();
// Cannot do anything with `foo` without knowing the memory layout under this
// void pointer.

...

Even the data type is known, it's unwise to cast the *void to a corresponding C data type since the memory layout may be different. That is, the memory layout of the following struct Foo in Rust:

// No `#[repr(C)]` here!
struct Foo {
    bar: i32,
    baz: f32,
    qux: [u32; 5],
}

may be different from the the following struct Foo in C

typedef struct {
  int32_t bar;
  float baz;
  uint32_t qux[5];
} Foo;

Therefore, it's dangerous to cast a void pointer converted by a Rust-style Foo pointer to a C-style Foo pointer to operate it, without marking struct Foo with #[repr(C)] in Rust.

...

Foo* foo = (Foo*) get_foo();
// This is very likely to fail since `foo->qux` may point to a different
// address as expected!
foo->qux[1] = 100;

...

Why we need to use Opaque Data Type

The reason why we need to use a opaque data type is well described in Opaque pointer and Opaque data type on Wiki page. In brief, it gives us the flexibility to change the underlying implementation without changing the interface or the need to recompile the code using the hidden data.

For example, the following C code

...

void* res = get_resource();

...

set_something_to_resource(res, ...)

...

based on the following Rust code

struct Foo {
    bar: i32,
    baz: f32,
    qux: [u32; 5],
}

impl Foo {
  fn new() -> Self {
      ...
  }
}

trait SetSomething {
    fn set_something(...);
}

impl SetSomething for Foo {
    fn set_something(...) {
        ...
    }
}

...

#[no_mangle]
pub extern "C" fn get_resource() -> *mut c_void {
    Box::into_raw(Box::new(Foo::new())) as *mut _
}

#[no_mangle]
pub extern "C" fn set_something_to_resource(ptr: *mut c_void, ...) {
    let foo = unsafe { &mut *(ptr as *mut Foo) };
    foo.set_something(...);
    ...
}

...

doesn't need to be modified if we change the Rust code into

struct Bar {
    foo: u32,
    baz: f64,
    qux: [i32; 10],
}

impl Bar {
  fn new() -> Self {
      ...
  }
}


trait SetSomething {
    fn set_something(...);
}

impl SetSomething for Bar {
    fn set_something(...) {
        ...
    }
}

...

#[no_mangle]
pub extern "C" fn get_resource() -> *mut c_void {
    Box::into_raw(Box::new(Bar::new())) as *mut _
}

#[no_mangle]
pub extern "C" fn set_something_to_resource(ptr: *mut c_void, ...) {
    let bar = unsafe { &mut *(ptr as *mut Bar) };
    bar.set_something(...);
    ...
}

...

The real-world example I've seen is Handle class. Here is the discussion for Windows' Handle type. The basic idea for Windows' Handle can be found here. One simple example for that is to open a file.

Here is better example for us. This example returns a handle from Rust code and it will be called in C/C++ code. That is exactly what we want to do in this post.

Sample Code

  • resource.rs: List two Rust structures:
    • Opaque: This is a opaque data type for external code.
    • Transparent: This is a transparent data type for external C code(marked with #[repr(C)]).
  • ext.rs: Interface to C
    • Operations for non-interoperable data: get_opaque, operate_opaque, return_opaque
    • Operations for interoperable data: get_transparent, return_transparent
      • You can operate the underlying values of the data directly!
  • ext.h: Header containes the interfaces for the Rust library
  • sample.c: Sample C code to operate above data
  • sample.cpp Sample C++ code to operate above data
  • Makefile: Build the sample by $ make. Clean files by $ make clean
#if !defined(EXT_H)
#define EXT_H
#include <stdint.h> // uint32_t
typedef struct {
uint32_t value;
} Transparent;
typedef void* Handle;
#if defined(__cplusplus)
extern "C" {
#endif
extern Transparent* get_transparent();
extern void return_transparent(Transparent* transparent);
extern Handle get_opaque();
extern void return_opaque(Handle opaque);
extern void operate_opaque(Handle opaque, uint32_t value);
#if defined(__cplusplus)
}
#endif
#endif // EXT_H
use resource::{Opaque, Transparent};
use std::os::raw::c_void;
mod resource;
// Interface to a non-interoperable resource
type Handle = *mut c_void;
#[no_mangle]
pub extern "C" fn get_opaque() -> Handle {
// 1. Create a Opaque instance in stack by `Opaque::new()`
// 2. Copy the Opaque instance from stack into heap by `Box::new(...)`
// 3. Consume the `Box` and return the wrapped raw pointer
let boxed_opa = Box::new(Opaque::new());
println!("leak opaque @ {:p}", boxed_opa.as_ref());
Box::into_raw(boxed_opa) as Handle
// Box::into_raw(Box::new(Opaque::new())) as *mut _
}
#[no_mangle]
pub extern "C" fn operate_opaque(opa: Handle, value: i32) {
assert!(!opa.is_null());
println!("operate leaked opaque @ {:p}", opa);
// 1. Cast a void pointer to a Opaque pointer
// 2. Create a mutable Opaque reference from a Opaque pointer
let opaque = unsafe { &mut *(opa as *mut Opaque) };
opaque.add(value);
}
#[no_mangle]
pub extern "C" fn return_opaque(opa: Handle) {
assert!(!opa.is_null());
println!("retake leaked opaque @ {:p}", opa);
// 1. Cast a void pointer to a Opaque pointer
// 2. Construct a box from a Opaque pointer
let opaque = unsafe { Box::<Opaque>::from_raw(opa as *mut _ /* Opaque */) };
// The box will be dropped after program runs out of the scope, or we can
// call drop here directly.
drop(opaque);
}
// Interface to a interoperable resource
#[no_mangle]
pub extern "C" fn get_transparent() -> *mut Transparent {
// 1. Create a Transparent instance in stack by `Transparent::new()`
// 2. Copy the Transparent instance from stack into heap by `Box::new(...)`
// 3. Consume the `Box` and return the wrapped raw pointer
let boxed_trans = Box::new(Transparent::new());
println!("leak transparent @ {:p}", boxed_trans.as_ref());
Box::into_raw(boxed_trans)
// Box::into_raw(Box::new(Transparent::new()))
}
#[no_mangle]
pub extern "C" fn return_transparent(tran: *mut Transparent) {
assert!(!tran.is_null());
println!("retake leaked transparent @ {:p}", tran);
// 1. Cast a void pointer to a Transparent pointer
// 2. Construct a box from a Transparent pointer
let transparent = unsafe { Box::<Transparent>::from_raw(tran) };
// The box will be dropped after program runs out of the scope, or we can
// call drop here directly.
drop(transparent);
}
all:
# Build a static library from the Rust file
rustc --crate-type=staticlib ext.rs
# Compile the C file with the static library
# gcc -o sample-c sample.c libext.a
gcc -o sample-c sample.c -L. -lext
./sample-c
# g++ -o sample-cpp sample.cpp libext.a
g++ -o sample-cpp sample.cpp -L. -lext
./sample-cpp
clean:
rm libext.a
rm sample-c
rm sample-cpp
// Non-interoperable resource
pub struct Opaque {
value: i32,
}
impl Opaque {
pub fn new() -> Self {
Opaque { value: 100 }
}
pub fn add(&mut self, value: i32) {
self.value += value;
}
pub fn get_value(&self) -> i32 {
self.value
}
}
impl Drop for Opaque {
fn drop(&mut self) {
println!("drop a Opaque with value: {}", self.get_value());
}
}
// Interoperable resource
#[repr(C)]
pub struct Transparent {
value: i32,
}
impl Transparent {
pub fn new() -> Self {
Transparent { value: 100 }
}
pub fn get_value(&self) -> i32 {
self.value
}
}
impl Drop for Transparent {
fn drop(&mut self) {
println!("drop a Transparent with value: {}", self.get_value());
}
}
#include "ext.h" // Types and in the external library
#include "stdio.h" // printf
int main() {
Transparent* transparent = get_transparent();
printf("get a transparent @ %p\n", transparent);
transparent->value = 200;
return_transparent(transparent);
// Now transparent's memory is freed and it's a dangling pointer.
Handle opaque = get_opaque();
printf("get a opaque @ %p\n", opaque);
operate_opaque(opaque, 500);
return_opaque(opaque);
// Now opaque's memory is freed and it's a dangling pointer.
return 0;
}
#include "ext.h" // Types and in the external library
#include <iostream> // printf
#include <memory> // std::unique_ptr
int main() {
std::unique_ptr<Transparent, decltype(&return_transparent)>
transparent(get_transparent(), return_transparent);
printf("get a transparent @ %p\n", transparent.get());
transparent->value = 200;
std::unique_ptr<void, decltype(&return_opaque)>
opaque(get_opaque(), return_opaque);
printf("get a opaque @ %p\n", opaque.get());
operate_opaque(opaque.get(), 500);
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment