(Ab)using .rodata for assets

Hey everyone.

This post is about a neat little trick that I stumbled upon.

While working on a C++ desktop app, I 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 don't want even more code generation in my pipeline.

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


Packaging the assets

The concept is pretty simple.
We'll build a static library that contains our assets, and then link it into our application.

To do that, we can use the incbin and dd pseudo-instructions.
We include a raw dump of our asset(s), and then store an end-marker and the size.

Basically it boils down to this code:

bits 64
section .rodata

global _foo_start
global _foo_end
global _foo_size

_foo_start:
    incbin "path/to/foo"

_foo_end:
    dd 1

_foo_size:
    dd _foo_end - _foo_start

Writing this code however is tedious and repetitive.
Fourtunately, NASM has support for macros:

%macro asset 2
    global _%1_start
    global _%1_end
    global _%1_size

    _%1_start:
        incbin %2

    _%1_end:
        dd 1

    _%1_size:
        dd _%1_end - _%1_start
%endmacro

bits 64
section .rodata

asset foo, "path/to/foo"
asset bar, "path/to/bar"
asset baz, "path/to/baz"

Gluing it to C++

To access this data from C++, we have to write a little header that declares the externs:

#pragma once

extern uint8_t const _foo_start asm("_foo_start");
extern uint8_t const _foo_end asm("_foo_end");
extern int const _foo_size asm("_foo_size");

And of course we can (and will) use macros here too.
I also decided to use helper methods to keep things clean outside of this file.

#define asset(name) \
    extern uint8_t const _ ## name ## _start asm("_" #name "_start"); \
    extern uint8_t const _ ## name ## _end asm("_" #name "_end"); \
    extern int const _ ## name ## _size asm("_" #name "_size"); \
    \
    namespace assets { \
        [[nodiscard]] void const* name() { \
            return (void const*)&_ ## name ## _start; \
        } \
        [[nodiscard]] int name ## _size() { \
            return _ ## name ## _size; \
        } \
    }

asset(foo)
asset(bar)
asset(baz)

Now we can simply refer to the asset using assets::foo() and assets::foo_size() anywhere.

If we ever need to modify an asset, we can just copy it to the heap:

void* mutableFoo = malloc(assets::foo_size());
assert(mutableFoo);
memcpy(mutableFoo, assets::foo(), assets::foo_size());

Make it build

Now that the code is written, we need to integrate all of this with CMake.

First, we make sure that NASM is enabled and supported:

enable_language(ASM_NASM)

if(NOT CMAKE_ASM_NASM_COMPILER_LOADED)
    message(FATAL_ERROR "oof")
endif()

Then we declare the static asset library.
The .asm file extension matters. CMake will use as/gas for .s files.

add_library(
    assets STATIC
    ./assets.asm
)

target_include_directories(
    assets PRIVATE
    ../../assets
)

Now, finally, the whole thing gets linked into the main application:

+ add_subdirectory(assets)

add_executable(foo ...)

target_link_libraries(
    foo
+   assets    
    ...
)

...

Final Result

Here's a little example using Dear ImGui.

I'm using the same code as above.

My assembly file includes the assets:

asset noto_sans_regular, "fonts/NotoSans-Regular.ttf"
asset noto_sans_italic, "fonts/NotoSans-Italic.ttf"
asset noto_sans_black, "fonts/NotoSans-Black.ttf"
asset noto_sans_bold, "fonts/NotoSans-Bold.ttf"
asset noto_sans_black_italic, "fonts/NotoSans-BlackItalic.ttf"
asset noto_sans_bold_italic, "fonts/NotoSans-BoldItalic.ttf"

And my header declares them:

asset(noto_sans_regular)
asset(noto_sans_italic)
asset(noto_sans_black)
asset(noto_sans_bold)
asset(noto_sans_black_italic)
asset(noto_sans_bold_italic)

Then we copy the fonts to the heap (using the _copy helper here), and pass them to ImGui.
(I'm using black italic first to make the effect more visible)

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
);

// ...

et voilà: