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
embedstatement - 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:
-
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! -
File Must Exist: The file must exist at compile time. If the file is missing or unreadable, compilation will fail.
-
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.jsonconst 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.