(Ab)using .rodata for assets

This post is about a neat little trick that I stumbled upon while working on a C++ desktop app,
which may be helpful for a few of you out there.

For our app, we needed a way to embed custom fonts and icons right into the binary.
It had to compile on both Clang and GCC, and needed to work for Linux, Windows, and OSX.

Other languages like Java or C# make this trivial, but C++ lacks a “standard” way to embed things.
A bit of searching revealed that generating code with xxd -i might work,
but I didn’t want even more code generation in my pipeline.

Well, as it turns out, we can (ab)use a few lines of assembly to solve this problem.

The concept is pretty simple really once you know where to look.
All we need is an object and the incbin pseudo-instruction,
to include a raw dump of our asset-data, and then store an end-marker and the size.

Here’s the macro and inline-assembly we came up with.
(Alloy is the name of our internal utility library).

First, something to declare our new variables/helpers:

#define ALLOY_ASSET_DECL(name) \
    extern unsigned char const _ ## name ## _data[] __asm__("_" #name "_data"); \
    extern unsigned char const* _ ## name ## _end __asm__("_" #name "_end"); \
    extern uint32 const _ ## name ## _size __asm__("_" #name "_size"); \
    \
    void const* name(); \
    int32 name ## _size(); \
    void* name ## _copy();

Then, the actual code for including the asset and assigning end/size markers:

#ifdef ALLOY_OS_DARWIN
#  define ALLOY_ASSET_SECTION ".const_data"
#else
#  define ALLOY_ASSET_SECTION ".section .rodata"
#endif

#define ALLOY_ASSET_OBJ(name, path) \
    __asm__( \
        ALLOY_ASSET_SECTION "\n"\
        \
        ".global " "_" #name "_data\n" \
        ".balign 8\n" \
        "_" #name "_data:\n" \
        ".incbin " "\"" path "\"\n" \
        \
        ".global _" #name "_end\n" \
        ".balign 1\n" \
        "_" #name "_end:\n" \
        ".byte 1\n" \
        \
        ".global _" #name "_size\n" \
        ".balign 8\n" \
        "_" #name "_size:\n" \
        ".int _" #name "_end - _" #name "_data\n" \
        ".balign 8\n" \
        \
        ".text\n" \
    ); \
    \
    void const* name() { \
        return (void const*)_ ## name ## _data; \
    } \
    int32 name ## _size() { \
        return _ ## name ## _size; \
    } \
    \
    void* name ## _copy() { \
        void* memory = malloc(name ## _size()); \
        if (memory == nullptr) { \
            return nullptr; \
        } \
        \
        memcpy(memory, name(), name ## _size()); \
        return memory; \
    }

Now all we need to do is create an assets.hpp and assets.cpp,
and use these macros to declare and define our assets.

For example, let’s include a couple of fonts:

#pragma once

namespace assets {
    ALLOY_ASSET_DECL(noto_sans_regular)
    ALLOY_ASSET_DECL(noto_sans_italic)
    ALLOY_ASSET_DECL(noto_sans_black)
    ALLOY_ASSET_DECL(noto_sans_bold)
    ALLOY_ASSET_DECL(noto_sans_black_italic)
    ALLOY_ASSET_DECL(noto_sans_bold_italic)
}
#include "assets.hpp"

namespace assets {
    ALLOY_ASSET_OBJ(noto_sans_regular, "fonts/NotoSans-Regular.ttf")
    ALLOY_ASSET_OBJ(noto_sans_italic, "fonts/NotoSans-Italic.ttf")
    ALLOY_ASSET_OBJ(noto_sans_black, "fonts/NotoSans-Black.ttf")
    ALLOY_ASSET_OBJ(noto_sans_bold, "fonts/NotoSans-Bold.ttf")
    ALLOY_ASSET_OBJ(noto_sans_black_italic, "fonts/NotoSans-BlackItalic.ttf")
    ALLOY_ASSET_OBJ(noto_sans_bold_italic, "fonts/NotoSans-BoldItalic.ttf")
}

And then pass them to ImGui as a little GUI example:

#include "assets.hpp"

// ...

auto& io = ImGui::GetIO();

io.Fonts->AddFontFromMemoryTTF(
    assets::noto_sans_black_italic_copy(),
    assets::noto_sans_black_italic_size(),
    18.0f
);

io.Fonts->AddFontFromMemoryTTF(
    assets::noto_sans_regular_copy(),
    assets::noto_sans_regular_size(),
    18.0f
);

// ...

…and there we go!

screenshot