Skip to content

Instantly share code, notes, and snippets.

@dwilliamson
Created May 19, 2019 21:46
Show Gist options
  • Save dwilliamson/1d5e811f81c5456d8eaf0ed62982a6cc to your computer and use it in GitHub Desktop.
Save dwilliamson/1d5e811f81c5456d8eaf0ed62982a6cc to your computer and use it in GitHub Desktop.
Command buffers
// LIB
using Buffer = std::vector<uint8_t>;
// LIB
struct Command
{
int size;
// NOTE: This *does not* need to be virtual - it's just the way std::function achieves it...
virtual void Call() = 0;
};
// USER
void AddCommandA(Buffer& buffer, int x, int y, int z)
{
buffer << [x, y, z]
{
DoSomethingWith(x, y, z);
};
}
// LIB
void ExecCommands(const Buffer& buffer)
{
const uint8_t* start = buffer.data();
const uint8_t* end = start + buffer.size();
while (start < end)
{
Command* command = (Command*)start;
command->Call();
start += command->size;
}
}
// Anything marked `USER` is something the user has to write and maintain.
// Anything marked `LIB` is in the library provided by the command buffer authors.
// USER
enum class CommandID
{
A, B, C, ...
};
// LIB
struct Command
{
Command() = default;
Command(CommandID id) : id(id) { }
CommandID id;
};
// USER
struct CommandAParams : public Command
{
static const CommandID ID = CommandID::A;
CommandAParams() : Command(ID) { }
int x, y, z;
};
// LIB
using Buffer = std::vector<uint8_t>;
// USER
void AddCommandA(Buffer& buffer, int x, int y, int z)
{
CommandAParams params;
params.x = x;
params.y = y;
params.z = z;
AddCommand(buffer, params);
}
// USER
void ExecCommands(const Buffer& buffer)
{
const uint8_t* start = buffer.data();
const uint8_t* end = start + buffer.size();
while (start < end)
{
CommandID id = (CommandID)*start;
switch (id)
{
case CommandID::A:
CommandAParams params = *(CommandAParams*)start;
DoSomethingWith(params.x, params.y, params.z);
start += sizeof(params);
break;
case CommandID::B:
...
}
}
}
@blackball
Copy link

Hi, I am not sure how the following codes work, could you explain ?

// USER
void AddCommandA(Buffer& buffer, int x, int y, int z)
{
  // How the following codes work ?
  buffer << [x, y, z]
  {
    DoSomethingWith(x, y, z);
  };
}

Thanks!

@dwilliamson
Copy link
Author

It's something like:

using Buffer = std::vector<std::function>;

template <class Lambda>
Buffer& operator << (Buffer& buffer, Lambda&& lambda)
{
   buffer.push_back(std::forward(lambda));
   return buffer;
}

But in reality Buffer is my own thread-safe queue and I have my own implementation of std::function that allocates all needed memory from within the queue, leading to no dynamic allocations.

@blackball
Copy link

blackball commented Jul 26, 2020

Thanks for your reply. I tried to write a demo based on your snippets, but still can not get it run. Could you provide a minimal demo for me to get the whole idea? Thanks very much!

@blackball
Copy link

Ha, I guess I have figured it out. Thanks!

#include <functional>
#include <iostream>
#include <vector>

using Commands = std::vector<std::function<int()>>;

template <class Lambda>
void operator<<(Commands &cmds, Lambda lambda) {
    cmds.push_back(lambda);
}

void AddA(Commands &cmds, int a, int b) {
    cmds << [a, b]() -> int {
        std::cout << a << "\n";
        return a + b;
    };
}

int main(int argc, char *argv[]) {
    Commands cmds;
    AddA(cmds, 1, 2);
    AddA(cmds, 2, 2);
    AddA(cmds, 3, 2);

    for (const auto &f : cmds) {
        f();
    }
    return 0;
}

@dwilliamson
Copy link
Author

Pretty much! Though note your lambda syntax can be simpler:

    cmds << [a, b] {
        std::cout << a << "\n";
        return a + b;
    };

My rule of thumb is: never bind [=], [&], [this] or pointers/references. You don't want the command to outlive the lifetime of the object you capture. Any references to objects go with an ID that's looked up inside the lambda; the C++ equivalent of this is capturing a smart pointer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment