Asset Embedding

Conjure allows you to embed file contents directly into your binary at compile time using the embed keyword. This is useful for bundling assets like shaders, configuration files, images, or any other data that your program needs at runtime.

Basic Usage

The embed keyword reads a file at compile time and embeds its contents into your program. The embedded data is stored as a static constant, so each unique file is only embedded once, even if referenced multiple times.

// Embed a text file as a string
const shader string = embed "assets/shader.glsl"

// Embed a binary file as a byte slice
const imageData []u8 = embed "assets/icon.png"

Type Inference

The embed keyword automatically infers the appropriate type based on how it’s used:

  • When assigned to a string, the file contents are embedded as a UTF-8 string
  • When assigned to a []u8, the file contents are embedded as raw bytes
  • If no type context is available, it defaults to []u8
// Explicit string type
const config string = embed "config.json"

// Explicit byte slice type
const binary []u8 = embed "data.bin"

// Type inference from left-hand side
var template string = embed "template.html"

Path Resolution

Embed paths can be either relative or absolute:

  • Relative paths are resolved relative to the source file containing the embed statement
  • Absolute paths are used as-is
// Relative to the current source file
const localShader = embed "./shader.glsl"

// Relative to a subdirectory
const data = embed "assets/data.bin"

// Absolute path (not recommended for portability)
const absData = embed "/usr/share/myapp/data.bin"

Compile-Time Requirements

The embed keyword has several important compile-time requirements:

  1. String Literals Only: The file path must be a string literal known at compile time. You cannot use variables or computed paths.

    // ✓ Valid - string literal
    const data = embed "file.txt"
    
    // ✗ Invalid - variable path
    var path = "file.txt"
    const data = embed path  // ERROR!
  2. File Must Exist: The file must exist at compile time. If the file is missing or unreadable, compilation will fail.

  3. UTF-8 Validation: When embedding as a string, the file contents must be valid UTF-8. Binary files should use []u8.

Global Deduplication

Conjure ensures that each unique file is only embedded once in the generated binary, regardless of how many times it’s referenced in your code. This prevents code bloat and reduces binary size.

func processShader() {
    // Even if called in a loop, the shader is only embedded once
    const shader = embed "shader.glsl"
    // ... use shader ...
}

The embedded data is stored as a static constant in the generated C code, and all references point to the same data.

Smart Caching

The compiler caches embedded file contents and tracks their modification times. During incremental builds:

  • Files are only re-read if their modification time has changed
  • Unchanged files use cached contents
  • This makes rebuilds faster when most assets haven’t changed

Working with Embedded Data

String Data

Embedded strings work like any other string in Conjure - they’re managed by the runtime, support string interpolation, and have a .len property.

const readme string = embed "README.md"

func printReadme() {
    io.println("README length: #{readme.len} bytes")
    io.println(readme)
}

Binary Data

Embedded binary data is stored as a byte slice and can be used like any other slice.

const imageData []u8 = embed "icon.png"

func getImageSize() usize {
    return imageData.len
}

func getPixelByte(index usize) u8 {
    return imageData[index]
}

Best Practices

Organize Assets

Keep your embedded assets in a dedicated directory to make them easy to manage:

src/
  main.cjr
assets/
  shaders/
    vertex.glsl
    fragment.glsl
  data/
    config.json
const vertexShader = embed "../assets/shaders/vertex.glsl"
const fragmentShader = embed "../assets/shaders/fragment.glsl"
const config = embed "../assets/data/config.json"

Be Mindful of Size

While Conjure can embed files of any size, embedding very large files (> 10MB) will increase your binary size significantly and may impact compilation time. The compiler will warn you about large embeds.

For very large assets, consider:

  • Loading them at runtime instead
  • Using compression
  • Splitting into smaller chunks

Use Descriptive Names

Give embedded constants descriptive names that indicate their purpose:

// Good - clear and descriptive
const fragmentShader string = embed "fragment.glsl"
const defaultConfig string = embed "config.json"

// Less clear
const data1 = embed "file1.bin"
const stuff = embed "thing.txt"

Example: OpenGL Shader Loading

Here’s a complete example of using embed to load GLSL shaders:

import "conjure/io" as io
import "./opengl" as gl

const vertexShaderSource string = embed "shaders/vertex.glsl"
const fragmentShaderSource string = embed "shaders/fragment.glsl"

func createShaderProgram() u32 {
    // Compile vertex shader
    var vertexShader = gl.createShader(gl.VERTEX_SHADER)
    gl.shaderSource(vertexShader, vertexShaderSource)
    gl.compileShader(vertexShader)

    // Compile fragment shader
    var fragmentShader = gl.createShader(gl.FRAGMENT_SHADER)
    gl.shaderSource(fragmentShader, fragmentShaderSource)
    gl.compileShader(fragmentShader)

    // Link program
    var program = gl.createProgram()
    gl.attachShader(program, vertexShader)
    gl.attachShader(program, fragmentShader)
    gl.linkProgram(program)

    return program
}

The shader sources are embedded at compile time, so there’s no need to ship separate shader files with your application or handle file I/O at runtime.

Future: Compile-Time Processing

In the future, Conjure may support compile-time function execution, which would allow you to process embedded files at compile time:

// Future planned feature (not yet implemented)
const model = gltf.process(embed "model.glb")
const compressedData = compress(embed "large_file.txt")

This would enable powerful build-time asset processing pipelines directly in your Conjure code.