Skip to content

Instantly share code, notes, and snippets.

@tomaka tomaka/problem.md Secret
Last active Feb 22, 2019

Embed
What would you like to do?
Rust problem

Here is a pattern that you would do in C and that you can't reproduce in Rust without a relatively big overhead.

(note that everything is pseudo-code, these are not the actual vulkan functions names and a lot of parameters are missing)

How to use Vulkan in C/C++

For the sake of this example, let's create a program and a command buffer:

unsigned long long program_id;
vkCreateProgram(vulkan_context, &program_id);

unsigned long long cb_id;
vkCreateCommandbuffer(vulkan_context, &cb_id);

The API manipulates identifiers whose meanings are implementation-defined, but in practice they are probably just raw pointers.

A Vulkan "program" contains code that is executed by the GPU and tells it how it should render 3D objects. A "command buffer" is a list of commands that the GPU must execute. In order to tell the GPU to draw something, we need to add an entry in the command buffer, and this entry contains the program.

vkCommandBufferStart(cb_id);
vkCommandBufferBindProgram(cb_id, program_id);
vkCommandBufferDraw(cb_id);
vkCommandBufferEnd();

This operation stores a pointer to the program in the commands buffer.

The Vulkan API is very low level, and one of its particularities is that you (the programmer) need to make sure that you don't modify or delete the program while the command buffer is still alive and/or in use. If you do that, you will most likely get a crash.

Translating that to Rust

In order to write a safe Rust wrapper around Vulkan, the idiomatic way would be to do this:

pub struct Program {
    id: u64,
}

impl Program {
    pub fn new(vulkan_context: &Context) -> Program {
        let mut id = mem::uninitialized();
        vkCreateProgram(vulkan_context, &mut id);
        Program { id: id }
    }
}

pub struct CommandBuffer<'a> {
    id: u64,
    dependencies: PhantomData<&'a ()>,
}

impl<'a> CommandBuffer<'a> {
    pub fn new_draw_command(vulkan_context: &Context, program: &'a Program) -> CommandBuffer<'a> {
        let mut id = mem::uninitialized();
        vkCreateCommandbuffer(vulkan_context, &mut id);
        vkCommandBufferStart(id);
        vkCommandBufferBindProgram(id, program.id);
        vkCommandBufferDraw(id);
        vkCommandBufferEnd();
        CommandBuffer { id: id, dependencies: PhantomData }
    }
}

Thanks to the lifetime, you're safe, as you prevent the user from deleting/modifying the program while the command buffer is alive.

However, using a lifetime also means you can't write that:

struct MyAssets {
    program: Program,
    command_buffer: CommandBuffer<???>,    // missing lifetime parameter
}

You must write that instead:

struct MyAssets1 {
    program: Program,
}

struct MyAssets2<'a> {
    command_buffer: CommandBuffer<'a>,
}

Why it is a problem

This example was just about programs and command buffers.

In the real Vulkan API, you have:

  • Devices
  • Programs (would contain a &Device)
  • Buffers (would contain a &Device)
  • Textures (would contain a &Device)
  • Fixed pipeline states (would contain a &Device)
  • Program pipelines (would contain a &Program and several &FixedPipelineStates)
  • Descriptor sets (would contain several &Buffers and &Textures, and a &ProgramPipeline)
  • Command buffers (would contain a &DescriptorSet)
  • (some others like fences, samples or queues that I'm not including for the sake of simplicity)

Because you can't store the borrower and borrowed in the same struct, you would end up with something like this:

struct Device {
    device: Device,
}

struct MyAssets1<'a> {
    programs: Vec<Program<'a>>,
    buffers: Vec<Buffer<'a>>,
    textures: Vec<Texture<'a>>,
}

struct MyAssets2<'a> {
    program_pipelines: Vec<ProgramPipeline<'a>>,
}

struct MyAssets3<'a> {
    descriptor_sets: Vec<DescriptorSet<'a>>,
}

struct MyAssets4<'a> {
    command_buffers: Vec<CommandBufer<'a>>,
}

Meanwhile in C/C++, you would just put everything in a big struct and handle the lifetimes in an unsafe manner.

The bigger problem is that the fact that you have five different structs that depend on one another "propagates" thoughout the API layers.

If you write a library that abstracts over vulkan-rs, you will need to have five different structs as well. If you write another library that abstracts over that library, you will need to have five different structs as well. The fact that you use Vulkan should be an implementation detail, and it would be confusing if the user had to create 5 randomly-named structs on the stack just to make it work.

The overhead

As far as I know the only way to solve this problem for the moment while remaining safe is to use a reference counter (Arc). Alternatively there is a known trick with RefCell to bypass the restriction, but it's not practical to use.

The overhead of using an Arc is big compared to a simple PhantomData<&'a ()>, because:

  • You do a memory allocation.
  • You do atomic operations.
  • You increase the chances of cache misses, as the object identifiers are on the heap.

All while a PhantomData is totally free and doesn't even exist at all at runtime.

Conclusion

I don't have any suggestion about how to solve this problem (I have already thought about this problem in the past).

For the moment my plan for a potential Vulkan wrapper library is to use generic pointers, like this:

pub struct CommandBuffer<P> where P: Deref<Program> {
    command_buffer: u64,
    program: P,
}

So that the user can choose between using a &'a Program (low-overhead, low usability) or a Arc<Program> (high overhead, high usability). But this solution isn't great.

@Ericson2314

This comment has been minimized.

Copy link

commented Sep 6, 2015

I've long wanted "existential lifetimes", which are precisely for things like this. The usage would look something like:

struct MyAssets(exists<'a> (Program + 'a, CommandBuffer<'a>));

Your example reminds me that we'd probably want some extra syntax so nominal types benefit, as opposed to forcing the use of tuples.

[FYI, that I don't see a way to make cycles work, so

struct MyAssets(exists<'a> (Program, CommandBuffer<'a>) + 'a);

would not fly. But I don't think that affects your use-case.]

@kylone

This comment has been minimized.

Copy link

commented Sep 18, 2015

Just wondering: could you use Rc instead of Arc for this?

@holmesmr

This comment has been minimized.

Copy link

commented Oct 1, 2015

I don't think Rc is desirable; since Rc is not Sync or Send it's essentially thread-local. I'm not sure whether in the examples this is necessarily restricting, but certainly since Vulkan is meant to enable greater parallelism in render pipelines, it certainly shouldn't be the only choice.

@Manishearth

This comment has been minimized.

Copy link

commented Feb 14, 2016

@Ericson2314 this exists, for<'a> CommandBuffer<'a> will work if it's a trait

@mark-buer

This comment has been minimized.

Copy link

commented Feb 19, 2016

@Manishearth Is the "for" keyword in for<'a> X<'a> documented/described someplace?

@Mixthos

This comment has been minimized.

Copy link

commented Apr 21, 2016

I created a macro for this. It's not completely safe, but it's usable: self-ref (works in Rust 1.9 and up)

@diwic

This comment has been minimized.

Copy link

commented Jun 27, 2016

I've also made a crate now. It's alpha quality right now, and ergonomics and performance could probably be improved, but it should generate a safe abstraction. Here it is: refstruct

@Mixthos, you might be interested in checking it out?

@neuronsguy

This comment has been minimized.

Copy link

commented Mar 24, 2017

Any word on what the best way to deal with this problem is currently? It seems to be a common feature of wrapping low-level C apis (like drivers).

@oconnor663

This comment has been minimized.

Copy link

commented Sep 23, 2017

Could you use a collection of slab allocators hidden somewhere in the context, with objects holding each other's slab handles instead of unsafe pointers? Everything still ends up on the heap, but hopefully with better cohesion?

@matklad

This comment has been minimized.

Copy link

commented Nov 11, 2017

For posterity, Rental crate somewhat alleviates this problem.

@cramertj

This comment has been minimized.

Copy link

commented Mar 22, 2018

Checking back in here-- it's probably worth playing around w/ this an Pin (though I bet whatever discoveries are made in Rental will probably be applicable here).

@oconnor663

This comment has been minimized.

Copy link

commented May 14, 2018

Alternatively there is a known trick with RefCell to bypass the restriction, but it's not practical to use.

Also from the future, what was this trick?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.