Home > Blockchain >  rust emulating variadic serialisation of PODs
rust emulating variadic serialisation of PODs

Time:08-17

In C I have the following template:

template<typename UBO>
std::pair<uint, std::vector<std::byte>> SerializeUniform(UBO& ubo, uint binding)
{
    // Create raw binary buffers of the uniform data
    std::vector<std::byte> ubo_buffer(sizeof(UBO));
    memcpy(ubo_buffer.data(), (void*)&ubo, sizeof(UBO));
    return {binding, ubo_buffer};
}

inline void SerializeArguments(NECore::UniformBufferDataContainer& data) {}

template<typename T1, typename... Ts>
inline void SerializeArguments(
    NECore::UniformBufferDataContainer& data, T1& uniform, uint binding, Ts&... args)
{
    data.push_back(SerializeUniform(uniform, binding));
    SerializeArguments(data, args...);
}

template<class... Ubos>
void ModuleStorage::Draw(const NECore::RenderRequest& render_request, const Ubos&... args)
{
    size_t arg_num = _details::ArgNum(args...);
    NECore::UniformBufferDataContainer ubos;
    ubos.reserve(arg_num);
    _details::SerializeArguments(ubos, args...);

    if(render_request.image_outputs.empty())
        DrawToScreen(vk_meta_data.vulkan_data, render_request, ubos);
    else
       DrawOffScreen(vk_meta_data.vulkan_data, render_request, ubos);
}

A bit hard to read so let me walk you through them.

The first template takes a POD and copies it's data into a vector of bytes and then creates a tuple to associate the data with an integer.

The next template is the base case of a recursive template, no parameters, do nothing.

Next we have a recursive template, take the first two parameters, assumed to be a POD and an integer, And serialize the POD. Recurse on the tail.

I.e. this template allows me to serialize an arbitrary number of PODs.

Finally I have a variadic template that allows me to serialize any number of PODs.

You might be wondering why go through all this trouble. It;s so that I can write things like this:

    modules.Draw(
        {
            gltf_shader,
            {gallery.GetGpuMeshData(model_names[selected_model])},
            textures,
            ssbos
        },
        mvp, 0,
        gltf_info, 1);

This way the render command accepts any arbitrary number of uniform parameters which means I can use the same pattern and syntax to call any arbitrary shader my heart desires with any inputs I want (as long as they are byte compatible with the shader declaration)

I am porting this library to rust and I want to achieve a similar thing with macros. i.e. I want to be abble to define things such that I can call


draw(render_request, macro!(ubo1, 0, ubo2, 1))

Or better yet (but I am almost certain this cannot be done in rust)

draw(render_request, ubo1, 0, ubo2, 1)

I am having a very hard time trying to come up with the macro. The primary issue is, macros are not functions and rust doesn't support variadic arguments. I am not entirely sure how to define the macro to achieve what I want.

CodePudding user response:

The usual way will be to define a function that accepts a generic type implementing a trait, and implement the trait (with a macro) for tuples up to X elements.

For example:

pub trait Arguments {
    fn serialize(self, ubo: &mut UniformBufferDataContainer);
}

macro_rules! impl_arguments {
    (
        ( $first_generic:ident $($rest_generics:ident)* )
        ( $first_binding:ident $($rest_bindings:ident)* )
    ) => {
        // Impl for tuples with N elements.
        impl_arguments_impl!( ( $first_generic $($rest_generics)* ) ( $first_binding $($rest_bindings)* ) );
        // Recurse to impl for tuples with N - 1 and less elements.
        impl_arguments!( ( $($rest_generics)* ) ( $($rest_bindings)* ) );
    };

    // Recursion end condition.
    ( ( ) ( ) ) => {};
}
macro_rules! impl_arguments_impl {
    ( ( $($generics:ident)  ) ( $($bindings:ident)  ) ) => {
        impl<$($generics,) > Arguments for ( $( &'_ $generics, u32, )  ) {
            fn serialize(self, ubo: &mut UniformBufferDataContainer) {
                // Destructure the tuple to access individual arguments.
                #[allow(non_snake_case)]
                let ( $( $generics, $bindings, )  ) = self;
                $(
                    ubo.push_back(serialize_uniform($generics, $bindings));
                ) 
            }
        }
    };
}

impl_arguments!((H G F E D C B A) (binding8 binding7 binding6 binding5 binding4 binding3 binding2 binding1));

pub fn draw<Ubos: Arguments>(render_request: &mut RenderRequest, args: Ubos) {
    // ...
    args.serialize(&mut ubos);
    // ...
}

Then you call it like:

draw(render_request, (&a, 0, &b, 1));

CodePudding user response:

I managed to get it to work by hacking the vec! macro from the standard library

macro_rules! UBO {
    () => {Vec::<UniformBufferData>::new()};
    ($($ubo:expr, $binding : expr),* $(,)?) =>
    {
        [$(serialize_uniform(&$ubo, $binding)), ].to_vec()
    }
}

This lets you call the macro UBO!(dummy2, 0, dummy, 1, dummy3, 3)

Which expands into

[
    serialize_uniform(&dummy2, 0),
    serialize_uniform(&dummy, 1),
    serialize_uniform(&dummy3, 3),
]
.to_vec()

So now you can do:

draw(render_request, UBO!(var1, 0, var2, 2, var3, 7));

And it should work (UB and other shennanigans aside).

  • Related