# Wgpu-mojo: wgpu-native bindings

**URL:** <https://forum.modular.com/t/wgpu-mojo-wgpu-native-bindings/2957>\
**Category:** Community Showcase\
**Tags:** gpu, ffi\
**Created:** [April 11, 2026, 9:16am UTC](https://forum.modular.com/t/wgpu-mojo-wgpu-native-bindings/2957 "2026-04-11T09:16:09Z")\
**Posts on this page:** 6\
**Page:** 1

<div class="post-metadata">

**Author:** ![Hundo](https://avatars.discourse-cdn.com/v4/letter/h/c6cbf5/32.png) [@Hundo](https://forum.modular.com/u/Hundo)\
**Post date:** [April 11, 2026, 9:16am UTC](https://forum.modular.com/t/wgpu-mojo-wgpu-native-bindings/2957/1 "2026-04-11T09:16:09Z")

</div>

> **[GitHub - Hundo1018/wgpu-mojo](https://github.com/Hundo1018/wgpu-mojo)**
>
> Contribute to Hundo1018/wgpu-mojo development by creating an account on GitHub.

## Preface

This endeavor is deceptively simple: enabling Mojo programs to directly invoke [wgpu-native](https://github.com/gfx-rs/wgpu-native) to render a triangle.

It is termed “simple” because rendering a triangle constitutes the quintessential Hello World of graphics programming; its successful display signifies that the entire pipeline—from WGSL shader compilation and RenderPipeline instantiation to GPU command submission and window presentation—has been seamlessly integrated. Conversely, it is deemed “complex” because implementing C Foreign Function Interface (FFI) bindings within Mojo presents a labyrinth of subtle pitfalls at every juncture.

This article documents these intricacies.

* * *

## Results

Executing `pixi run hello` yields a window displaying an RGB gradient triangle:

```bash
pixi run example-triangle

```

 ![image](https://us1.discourse-cdn.com/flex001/uploads/modular/original/2X/4/49521043e19d46e63d51349630b8bab84cc73ccc.png)

Beneath this visual lies a chain of operations driven entirely by Mojo: GLFW window initialization, wgpu Surface creation, ShaderModule compilation, RenderPipeline construction, draw calls, and final presentation. Furthermore, a compute shader example (`pixi run example-compute`) successfully performs vector addition on 1,024 elements on an RTX 3060, yielding correct results.

* * *

## Usage

### Prerequisites

- Mojo ≥ 0.26.3 (nightly)
- [Pixi](https://prefix.dev/) package manager
- `ffi/lib/libwgpu_native.so` (the linux\_x86\_64 shared library for wgpu-native v29.0.0.0)

### Quick Start

```bash
# Compile the C bridge layer (one-time setup)
pixi run build-callbacks

# Execute the triangle example
pixi run example-triangle

# Run non-GPU tests
pixi run test

```

### Architectural Overview of the Hello Triangle

The WGSL shader is embedded directly within the Mojo source code as a `comptime` string constant:

```mojo
comptime TRIANGLE_WGSL = """
struct VertexOutput {
    @builtin(position) pos: vec4<f32>,
    @location(0) color: vec3<f32>,
}

@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
    var positions = array<vec2<f32>, 3>(
        vec2<f32>( 0.0, 0.5),
        vec2<f32>(-0.5, -0.5),
        vec2<f32>( 0.5, -0.5),
    );
    var colors = array<vec3<f32>, 3>(
        vec3<f32>(1.0, 0.0, 0.0),
        vec3<f32>(0.0, 1.0, 0.0),
        vec3<f32>(0.0, 0.0, 1.0),
    );
    var out: VertexOutput;
    out.pos = vec4<f32>(positions[idx], 0.0, 1.0);
    out.color = colors[idx];
    return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
    return vec4<f32>(in.color, 1.0);
}
"""

```

The main program skeleton follows:

```mojo
def main() raises:
    var inst = request_adapter()
    var device = inst.request_device()
    var canvas = RenderCanvas(inst, device, 800, 600, "wgpu-mojo: hello triangle")
    var shader = device.create_shader_module_wgsl(TRIANGLE_WGSL, "triangle")

    # ... Pipeline construction ...

    while canvas.is_open():
        canvas.poll()
        var frame = canvas.next_frame()
        # ... Encode render pass, draw(3, 1, 0, 0), submit ...
        canvas.present()

```

While the high-level API achieves a commendable degree of elegance, the true complexity resides within the omitted ellipses of the pipeline construction phase.

* * *

## Project Structure

```auto
wgpu/ High-level RAII wrappers (Device, Buffer, Texture, Pipeline…)
wgpu/_ffi/
  lib.mojo WGPULib — ~170 functions dispatched via OwnedDLHandle + dlsym
  types.mojo Type aliases, enums, and bitflags
  structs.mojo C struct layouts (descriptors, callback results)
ffi/
  wgpu_callbacks.c C bridge layer
  include/webgpu/ webgpu.h / wgpu.h headers
  lib/ .so libraries

```

The wgpu-native API comprises approximately 170 functions, each requiring a manually crafted `DLHandle.call[...]` invocation in `lib.mojo` with exact type correspondence. All the subsequent challenges stem from this foundational layer.

* * *

## Ten Pitfalls Encountered

### 1. `DLHandle.call` Returns Garbage for Structs Exceeding 16 Bytes

This represents the project’s most fundamental hurdle. Nearly every wgpu-native construction function accepts a large descriptor struct, such as `WGPURenderPipelineDescriptor`, `WGPUBindGroupDescriptor`, or `WGPUComputePipelineDescriptor`, which often span 40 to 120 bytes.

However, `DLHandle.call` fails to emit the `byval` attribute required by the SysV x86\_64 ABI when passing structs larger than 16 bytes. Consequently, the C side receives random data from the stack.

Empirical verification (via a custom C library with printf statements):

```auto
16 bytes (2 × uint64): C receives a=10, b=20 → sum=30 ✓
24 bytes (3 × uint64): C receives a=108586124556112, b=488, c=8 → FAIL

```

**Solution:** All wgpu functions accepting large structs must be wrapped in a pointer-based version within the C bridge layer:

```c
// Pattern in ffi/wgpu_callbacks.c
void wgpuDeviceCreateRenderPipeline_ptr(
    WGPUDevice device,
    const WGPURenderPipelineDescriptor *desc,
    WGPURenderPipeline *out
) {
    *out = wgpuDeviceCreateRenderPipeline(device, desc);
}

```

On the Mojo side, the struct is allocated on the heap via `alloc[WGPURenderPipelineDescriptor](1)`, populated, and then passed by pointer. This is not a temporary workaround but the only viable approach under current constraints.

### 2. Inability to Pass Mojo Functions as C Callbacks

The asynchronous wgpu-native API relies heavily on C function pointer callbacks, such as `WGPURequestDeviceCallback` passed to `wgpuAdapterRequestDevice`.

In Mojo 0.26 nightly, there is no mechanism to obtain a C-compatible function pointer from a Mojo function. The `fn` keyword is deprecated in this version, and `def` functions lack address-of syntax.

**Solution:** Maintain a separate C file (`wgpu_callbacks.c`) defining static C callbacks and result structures, along with a getter function for Mojo to retrieve the callback pointer:

```c
static void _wgpu_mojo_device_cb(
    WGPURequestDeviceStatus status,
    WGPUDevice device,
    WGPUStringView message,
    void* ud1, void* ud2
) {
    MojoDeviceResult* r = (MojoDeviceResult*)ud1;
    if (r) { r->device = (void*)device; r->status = (uint32_t)status; }
}

WGPURequestDeviceCallback wgpu_mojo_get_device_cb(void) {
    return _wgpu_mojo_device_cb;
}

```

Mojo invokes the getter to retrieve the pointer and passes it to wgpu-native. One must write a dedicated wrapper for every distinct callback type; this is unavoidable.

### 3. ASAP Destruction: Objects Vanish After Their Last Use

Mojo’s “As Soon As Possible” (ASAP) destruction policy causes `Movable` types to be deallocated immediately after their final use. This clashes profoundly with the temporal semantics of GPU programming.

Consider the triangle example: creating a `RenderPipeline` requires passing handles to a `PipelineLayout` and `ShaderModule`. The Mojo compiler, observing the `pl.handle()` call, assumes `pl`’s lifecycle has ended. However, wgpu-native only accesses this handle internally during `create_render_pipeline`.

```mojo
var pipeline = device.create_render_pipeline(desc)
// pl and shader are already ASAP-dropped before this line
// ↑ This may lead to crashes or validation errors

```

**Solution:** Utilize `_ = var^` to explicitly pin the destruction of the consumed value:

```mojo
var pipeline = device.create_render_pipeline(desc)
_ = pl^ // Defers pl's destruction until this line, ensuring safety
_ = shader^ // Same logic

```

This adheres to valid transfer semantics, not a hack. It necessitates a mental check at every `.handle()` call: “How long must this wrapper survive? Has the submission completed?”

Currently, the codebase contains a dozen such pins scattered across tests and examples.

### 4. Counterintuitive Initialization of `List[T]`

The instinctive approach to creating a list with a single element is:

```mojo
var cmds = List[OpaquePtr](cmd) // Compilation Error: no matching function

```

This syntax invokes the constructor accepting capacity, not the one initializing from elements. The correct approach is:

```mojo
var cmds: List[OpaquePtr] = [cmd] // Invokes __list_literal__ , works correctly

```

However, a constraint exists: `T` must be `Copyable`. Callback info structs containing raw pointers (e.g., `WGPUBufferMapCallbackInfo`) do not implement `Copyable` and cannot be placed in a `List`; they require manual `alloc`/`free` management. This limitation, regrettably, stems from lingering Pythonic paradigms.

### 5. `WGPUStringView` Lifetime: A Silent Bomb

In wgpu-native v29, all string parameters were transitioned to `WGPUStringView` (pointer + length), replacing the legacy null-terminated `const char*`.

The `str_to_sv` function constructs a `WGPUStringView` from a Mojo `String`, holding a raw pointer to the string’s internal buffer. If the `String`’s lifetime is shorter than that of the `WGPUStringView`, a use-after-free occurs:

```mojo
// Dangerous: String("vs_main") is a temporary value, prone to early destruction
var vs_entry = str_to_sv(String("vs_main"))

// Safe: The String is bound to a named variable with an explicit lifetime
var vs_name = String("vs_main")
var vs_entry = str_to_sv(vs_name)

```

This issue leaves no obvious trace in the code, surfacing only when the GPU receives garbled strings or triggers validation errors.

### 6. Enum Value Shifts from v27 to v29 Without Compile Errors

The wgpu-native v29 enum system inserted `BindingNotUsed=0x0` at the beginning, causing all subsequent numeric values to shift by one. Code relying on hardcoded numeric constants silently fails; binding types mismatch, GPU validation errors occur, yet the error messages offer no clue regarding the enum values.

For instance, the Storage buffer binding type was 2 in v27 but became 3 in v29. Confirming such discrepancies requires consulting the `webgpu.h` source code.

### 7. `wgpuQuerySetDestroy` Causes Double-Free in v29

In v29, `wgpuQuerySetDestroy` internally performs the drop operation, removing the resource from the registry. Subsequent calls to `wgpuQuerySetRelease` result in a double-free and program crash. The semantics of these two functions differed in v27, causing immediate breakage upon upgrade.

**Fix:** `QuerySet. __del__ ` should invoke `Release` exclusively, omitting `Destroy`.

### 8. Manual Compilation of `wgpu_callbacks.c`

This C bridge layer is not integrated into Pixi’s automated build pipeline (or requires a manual initial step):

```bash
gcc -shared -fPIC -o ffi/lib/libwgpu_mojo_cb.so ffi/wgpu_callbacks.c \
    -Iffi/include -Lffi/lib -lwgpu_native -Wl,-rpath,'$ORIGIN'

```

Failure to compile this beforehand results in silent `dlopen` failures at runtime, with error messages that may not point to the missing `.so` file.

### 9. All Types Alias to `OpaquePtr`

Currently, all GPU handles (`WGPUBufferHandle`, `WGPUTextureHandle`, `WGPUPipelineHandle`, etc.) are aliased as the same `OpaquePtr` type within the type system. Passing an incorrect handle yields no compile-time errors; only runtime wgpu validation errors (if enabled) will surface the issue.

### 10. Redundant `dlopen` Calls in Every Wrapper

Each GPU wrapper struct (`Device`, `Buffer`, `Texture`, etc.) instantiates a `WGPULib` object within its ` __init__ `, which in turn calls `OwnedDLHandle`, effectively invoking `dlopen` every time. Consequently, creating a `TextureView` in the render loop triggers a `dlopen` per frame. While functional, this is not a sound design.

* * *

## Recommendations for the Mojo Language

Having completed this binding, several areas where language-level support would significantly simplify C FFI implementation come to mind:

**1. Correct `DLHandle.call` to Emit SysV ABI `byval`**  
The generation of erroneous LLVM IR for structs exceeding 16 bytes passed by value is the root cause. If `DLHandle.call` correctly emits the `byval` attribute, the entire workaround of allocating descriptors to the heap and passing pointers could be dismantled.

**2. Provide Syntax for Obtaining C-Callable Function Pointers**  
A syntax akin to C’s `&my_callback` to obtain a C-compatible pointer from a Mojo function would allow direct passing to C APIs requiring callbacks, obviating the need for the entire `wgpu_callbacks.c` bridge layer.

**3. Promote the Use of `@align` Decorator**  
The `@align(N)` decorator allows alignment requirements to be specified directly on struct types, ensuring both stack and heap allocations adhere to them. This is particularly vital for GPU descriptors requiring specific alignment (e.g., 64-byte alignment for certain hardware TMA descriptors), offering greater safety than manual specification in every `alloc` call.

**4. Leverage the `with` Statement**  
Context managers (` __enter__ ` / ` __exit__ `) are fully functional in the current nightly build, with ` __exit__ ` executing even upon errors. This offers a more structured alternative to `_ = var^` pinning for scope-scoped resource cleanup. While current GPU wrappers cannot yet be wrapped as context managers due to holding non-copyable `OwnedDLHandle`, adjusting the ownership design would enable idioms like `with device:`.

**5. Enhanced Lifetime and Borrowing Tools**  
Currently, developers must manually track “how long this wrapper needs to survive” at every `.handle()` call and insert `_ = var^`. A mechanism akin to Rust’s borrow checker, or even lighter-weight scope annotations, would allow such lifetime errors to be caught at compile time.

* * *

## So…

This research was made possible by the exhaustive work already done in the WebGPU ecosystem. The existence of the WebGPU specification, wgpu-native, wgpu-rs, and wgpu-py provided the essential map for this exploration. Without these established resources, I wouldn’t even have a starting point to test the limits of Mojo’s FFI.

I also utilized Mojo AI Skills as a practical tool to handle the more tedious aspects of the binding process. It was efficient for generating boilerplate and sanity-checking Mojo’s evolving syntax, allowing me to focus on the actual architectural hurdles.

Ultimately, my role was to synthesize these resources, navigate the pitfalls, and bridge the gap—consuming a fair number of tokens along the way.

Moving forward, I intend to explore more elegant solutions for \_ = var^ (perhaps via Context Managers) and investigate align(N). I also plan to experiment with leveraging MAX’s computational power to forge a path directly into the rendering pipeline, or perhaps by implementing an input system and developing a bindgen. Regardless, there remains much to learn. Until next night, Mojicians! 🪄

---

<div class="post-metadata">

**Author:** ![owenhilyard](https://sea1.discourse-cdn.com/flex001/user_avatar/forum.modular.com/owenhilyard/32/41_2.png) [@owenhilyard](https://forum.modular.com/u/owenhilyard)\
**Post date:** [April 11, 2026, 2:22pm UTC](https://forum.modular.com/t/wgpu-mojo-wgpu-native-bindings/2957/2 "2026-04-11T14:22:59Z")

</div>

> [@Hundo](#):
>
> `DLHandle.call` Returns Garbage for Structs Exceeding 16 Bytes

Please file a bug for this.

> [@Hundo](#):
>
> Inability to Pass Mojo Functions as C Callbacks

A mojo `foo: def(Int) -> None` should be equivalent to a `void*(*foo)(ssize_t)`. If it’s not or you find other mismatches, please file bugs.

> [@Hundo](#):
>
> ASAP Destruction: Objects Vanish After Their Last Use

The solution to this is to use origins to notify the compiler that the handle requires information from `pl`.

> [@Hundo](#):
>
> Counterintuitive Initialization of `List[T]`

For `var cmds = List[OpaquePtr](cmd)`, the variadic constructor was causing some headaches, so we’ve moved using literals where possible as you found. The `Copyable` constraint is a result of conditional conformance not fully working, and that will be fixed in the future.

> they require manual `alloc`/`free` management

Mojo offers both RAII (which as far as I’m aware is what C++ and Rust bindings use) as well as linear types, which can be used to force users to call a specific function later on. In general, I would expect that a library like this can make the API fully safe and difficult to cause memory leaks with.

> [@Hundo](#):
>
> `WGPUStringView` Lifetime: A Silent Bomb

You’ll need to hold an origin to fix this, look at the stdlib `StringSpan` for how to do that.

> [@Hundo](#):
>
> Enum Value Shifts from v27 to v29 Without Compile Errors

Sadly we can’t really help here until automatic binding generation works.

> [@Hundo](#):
>
> Manual Compilation of `wgpu_callbacks.c`

You should be able to use pixi tasks for this.

> [@Hundo](#):
>
> All Types Alias to `OpaquePtr`

This should definitely be fixable using the “newtype” pattern, which would be a struct that only contains a single member and acts to enhance type safety.

> [@Hundo](#):
>
> Redundant `dlopen` Calls in Every Wrapper

You can do `mojo build -Xlinker -l libfoo.so` to link libfoo. You might want to look at doing that to cut down on dlopen if the library is mandatory.

> [@Hundo](#):
>
> Recommendations for the Mojo Language

I strongly agree on the first two. For the third one, you should always use aligns for anything that has alignment requirements. The capability to over-align exists for reasons of things like `Float32`, which you sometimes want 64-byte aligned for SIMD reasons but which doesn’t need to always be 64-byte aligned.

For 4 and 5, I think you missed that Mojo has a borrow checker (part of what powers the asap destruction), and you aren’t integrating with it while using unsafe constructs (OpaquePointer), which is causing a lot of your headaches. Rust will cause you similar pain if you tried to do this there, although you get some leniency due to drops only happening at scope closes. LLMs are really bad at utilizing it, especially around low level bindings like this, which might be why you missed it.

# Overall

Overall, I think this is a fantastic project to have in the ecosystem, and I’m happy to help guide you towards more idiomatic mojo. I know the lower level, ffi heavy stuff hasn’t been documented as well as it should have been, so this may be a sign we need better docs there.

---

<div class="post-metadata">

**Author:** ![sbrunk](https://sea1.discourse-cdn.com/flex001/user_avatar/forum.modular.com/sbrunk/32/155_2.png) [@sbrunk](https://forum.modular.com/u/sbrunk)\
**Post date:** [April 11, 2026, 4:57pm UTC](https://forum.modular.com/t/wgpu-mojo-wgpu-native-bindings/2957/3 "2026-04-11T16:57:33Z")

</div>

> [@owenhilyard](#):
>
> > [@Hundo](#):
> >
> > `DLHandle.call` Returns Garbage for Structs Exceeding 16 Bytes
> 
> Please file a bug for this.

I think the issue is already tracked and being worked on. Using the linker and `external_call` should help, but works only with `mojo compile`, not with `mojo run`.

> <https://github.com/modular/modular/issues/5846>
>
> \### Bug description
> 
> \### Behavior
> 
> I have the following program in Mojo:
> 
> \`\`\`
> fr…om sys.ffi import external\_call
> 
> @fieldwise\_init
> @register\_passable("trivial")
> struct MyColor:
> var r: UInt8
> var g: UInt8
> var b: UInt8
> var a: UInt8
> 
> def main():
> external\_call\["InitWindow", NoneType\](800, 600, "Hello my dear")
> while not external\_call\["WindowShouldClose", Bool\]():
> external\_call\["BeginDrawing", NoneType\]()
> external\_call\["ClearBackground", NoneType\](MyColor(0xff, 0xff, 0x00, 0xff)) # Red
> external\_call\["EndDrawing", NoneType\]()
> external\_call\["CloseWindow", NoneType\]()
> \`\`\`
> 
> I have linked dynamically against the library C library \[raylib\](https://www.raylib.com/) with \`-Xlinker -lraylib\`. I am expecting the background to be cleared with the color pink, but I am instead getting the color red.
> 
> In raylib the function is defined as \`void ClearBackground(Color color);\` and the struct is defined \`typedef struct Color { unsigned char r, g, b, a; } Color;\`.
> 
> Interestingly, if I replace the \`MyColor\` parameter with a \`UInt32\` I can get the expected color, i.e.
> \`\`\`
> external\_call\["ClearBackground", NoneType\](0xffff00ff) # Pink
> \`\`\`
> 
> I've checked \`size\_of\[MyColor\]\` and it is indeed \`4\`. Am I misunderstanding about how structs interact with \`external\_call\`?
> 
> It also appears that I am not the first person to notice this problem. In \[raylib-mojo\](https://github.com/andresnowak/raylib-mojo) the author wrapped the call to \`ClearBackground\` so that instead of passing the struct directly, a pointer to a struct is passed instead.
> 
> \### Steps to reproduce
> 
> See above
> 
> \### System information
> 
> \- Provide the system information by running \`pixi info\`.
> \`\`\`
> System
> \------------
> Pixi version: 0.63.2
> TLS backend: rustls
> Platform: linux-64
> Virtual packages: \_\_unix=0=0
> : \_\_linux=6.12.63=0
> : \_\_glibc=2.41=0
> : \_\_cuda=13.1=0
> : \_\_archspec=1=broadwell
> Cache dir: /home/mortenbh/.cache/rattler/cache
> Auth storage: /home/mortenbh/.rattler/credentials.json
> Config locations: No config files found
> 
> Global
> \------------
> Bin dir: /home/mortenbh/.pixi/bin
> Environment dir: /home/mortenbh/.pixi/envs
> Manifest dir: /home/mortenbh/.pixi/manifests/pixi-global.toml
> 
> Workspace
> \------------
> Name: MojoRaylib
> Version: 0.1.0
> Manifest file: /home/mortenbh/Projects/MojoRaylib/pixi.toml
> Last updated: 23-01-2026 23:59:56
> 
> Environments
> \------------
> Environment: default
> Features: default
> Channels: https://conda.modular.com/max-nightly, conda-forge
> Dependency count: 2
> Dependencies: mojo, modular
> Target platforms: linux-64
> Prefix location: /home/mortenbh/Projects/MojoRaylib/.pixi/envs/default
> \`\`\`
> 
> \- Provide version information for MAX (includes Mojo) by pasting the output of \`pixi list max\`.
> \`\`\`
> Name Version Build Size Kind Source
> max 26.2.0.dev2026012305 3.14release 5.88 MiB conda https://conda.modular.com/max-nightly
> max-core 26.2.0.dev2026012305 release 119.47 MiB conda https://conda.modular.com/max-nightly
> max-pipelines 26.2.0.dev2026012305 release 16.45 KiB conda https://conda.modular.com/max-nightly
> \`\`\`

> <https://github.com/modular/modular/issues/3144>
>
> \### Bug description
> 
> I'm not sure if this is something already know or if ther…e is a way to make it work, but I have found two bugs in the FFI of mojo (\_And also that it seems it is only possible to use register passable values, if one passes a pointer to c it is possible to use non register passable values, but for the return values coming from c into mojo a non register passable value struct causes memory errors\_.)
> 
> \- One is that it is not possible to pass structs from mojo to c, you can receive them (but only register passable) but not to pass them, it seems it is only possible to pass numbers (ex. I have a color struct with 4 values of uint8 and i pass the to c the values are corrupted but if i bitcast it to uint64 and pass it to c it does work, but with bigger structs this is not possible)
> 
> \- The other is that is seems to be possible to receive structs from c into mojo but it seems there are sometimes bug in macos where it crashes the program.
> 
> \### Steps to reproduce
> 
> \## Struct passing to C error
> \`\`\`mojo
> from sys.ffi import DLHandle, RTLD
> 
> alias RAYLIB\_PATH = "/home/linuxbrew/.linuxbrew/Cellar/raylib/5.0/lib/libraylib.so"
> 
> var raylib\_internal = DLHandle(RAYLIB\_PATH, RTLD.NOW)
> 
> @register\_passable("trivial")
> struct Color(CollectionElement):
> """Color, 4 components, R8G8B8A8 (32bit)."""
> 
> var r: UInt8
> var g: UInt8
> var b: UInt8
> var a: UInt8
>     
> @always\_inline
> fn \_\_init\_\_(inout self, r: UInt8, g: UInt8, b: UInt8, a: UInt8):
> """Constructs a Color from r, g, b, and a values."""
> self.r = r
> self.g = g
> self.b = b
> self.a = a
> 
> fn \_\_str\_\_(self) -\> String:
> return "Color(" + str(self.r) + ", " + str(self.g) + ", " + str(self.b) + ", " + str(self.a) + ")"
> 
> alias c\_raylib\_InitWindow = fn(width: Int32, height: Int32, title: UnsafePointer\[Int8\]) -\> None
> 
> var \_init\_window = raylib\_internal.get\_function\[c\_raylib\_InitWindow\]("InitWindow")
> 
> fn init\_window(width: Int32, height: Int32, title: String) -\> None:
> \_init\_window(width, height, title.unsafe\_ptr())
> 
> alias c\_raylib\_WindowShouldClose = fn() -\> Bool
> 
> var \_window\_should\_close = raylib\_internal.get\_function\[c\_raylib\_WindowShouldClose\]("WindowShouldClose")
> 
> fn window\_should\_close() -\> Bool:
> return \_window\_should\_close()
> 
> alias c\_raylib\_BeginDrawing = fn() -\> None
> 
> var \_begin\_drawing = raylib\_internal.get\_function\[c\_raylib\_BeginDrawing\]("BeginDrawing")
> 
> fn begin\_drawing() -\> None:
> \_begin\_drawing()
> 
> alias c\_raylib\_EndDrawing = fn() -\> None
> 
> var \_end\_drawing = raylib\_internal.get\_function\[c\_raylib\_EndDrawing\]("EndDrawing")
> 
> fn end\_drawing() -\> None:
> \_end\_drawing()
> 
> alias c\_raylib\_CloseWindow = fn() -\> None
> 
> var \_close\_window = raylib\_internal.get\_function\[c\_raylib\_CloseWindow\]("CloseWindow")
> 
> fn close\_window() -\> None:
> \_close\_window()
> 
> alias c\_raylib\_DrawText = fn(text: UnsafePointer\[Int8\], x: Int32, y: Int32, font\_size: Int32, color: Color) -\> None
> 
> var \_draw\_text = raylib\_internal.get\_function\[c\_raylib\_DrawText\]("DrawText")
> 
> fn draw\_text(text: String, x: Int32, y: Int32, font\_size: Int32, color: Color) -\> None:
> \_draw\_text(text.unsafe\_ptr(), x, y, font\_size, color)
> 
> alias c\_raylib\_DrawTextGood = fn(text: UnsafePointer\[Int8\], x: Int32, y: Int32, font\_size: Int32, color: UInt64) -\> None
> 
> var \_draw\_text\_good = raylib\_internal.get\_function\[c\_raylib\_DrawTextGood\]("DrawText")
> 
> fn draw\_text\_good(text: String, x: Int32, y: Int32, font\_size: Int32, owned color: Color) -\> None:
> \_draw\_text\_good(text.unsafe\_ptr(), x, y, font\_size, UnsafePointer.address\_of(color).bitcast\[UInt64\]()\[\])
> 
> 
> fn main():
> init\_window(800, 600, "Hello, World!")
> 
> while not window\_should\_close():
> begin\_drawing()
> 
> draw\_text\_good("Hello, World!", 190, 200, 20, Color(200, 200, 200, 255))
> draw\_text("Hello, World! This Doesn't work Because Color gets corrupted in C side", 190, 200, 20, Color(200, 200, 200, 255))
> 
> end\_drawing()
> 
> close\_window()
> \`\`\`
> 
> \## Mac error that crashes program, but in linux it works
> \`\`\`mojo
> from sys.ffi import DLHandle, RTLD
> 
> alias RAYLIB\_PATH = "/home/linuxbrew/.linuxbrew/Cellar/raylib/5.0/lib/libraylib.so"
> 
> var raylib\_internal = DLHandle(RAYLIB\_PATH, RTLD.NOW)
> 
> @register\_passable("trivial")
> struct Texture:
> var id: UInt32
> var width: Int32
> var height: Int32
> var mipmaps: Int32
> var format: Int32
> 
> fn init(
> inout self,
> id: UInt32,
> width: Int32,
> height: Int32,
> mipmaps: Int32,
> format: Int32,
> ):
> self.id = id
> self.width = width
> self.height = height
> self.mipmaps = mipmaps
> self.format = format
> 
> fn \_\_str\_\_(self) -\> String:
> return (
> "Texture("
> "id: "
> + str(self.id)
> + ", width:"
> + str(self.width)
> + ", height:"
> + str(self.height)
> + ", mipmaps: "
> + str(self.mipmaps)
> + ", format: "
> + str(self.format)
> + ")"
> )
> 
> alias c\_raylib\_InitWindow = fn(width: Int32, height: Int32, title: UnsafePointer\[Int8\]) -\> None
> 
> var \_init\_window = raylib\_internal.get\_function\[c\_raylib\_InitWindow\]("InitWindow")
> 
> fn init\_window(width: Int32, height: Int32, title: String) -\> None:
> \_init\_window(width, height, title.unsafe\_ptr())
> 
> alias c\_raylib\_LoadTexture = fn(file\_name: UnsafePointer\[Int8\]) -\> Texture
> 
> var \_load\_texture = raylib\_internal.get\_function\[c\_raylib\_LoadTexture\]("LoadTexture")
> 
> fn load\_texture(file\_name: String) -\> Texture:
> return \_load\_texture(file\_name.unsafe\_ptr())
> 
> fn main():
> init\_window(800, 600, "Hello, World!")
> 
> var xbox = load\_texture("resources/xbox.png")
> 
> print(xbox)
> \`\`\`
> \### mac output
> \`\`\`bash
> INFO: Initializing raylib 5.0
> INFO: Platform backend: DESKTOP (GLFW)
> INFO: Supported raylib modules:
> INFO: \> rcore:..... loaded (mandatory)
> INFO: \> rlgl:...... loaded (mandatory)
> INFO: \> rshapes:... loaded (optional)
> INFO: \> rtextures:. loaded (optional)
> INFO: \> rtext:..... loaded (optional)
> INFO: \> rmodels:... loaded (optional)
> INFO: \> raudio:.... loaded (optional)
> INFO: DISPLAY: Device initialized successfully
> INFO: \> Display size: 1728 x 1117
> INFO: \> Screen size: 800 x 600
> INFO: \> Render size: 800 x 600
> INFO: \> Viewport offsets: 0, 0
> INFO: GLAD: OpenGL extensions loaded successfully
> INFO: GL: Supported extensions count: 43
> INFO: GL: OpenGL device information:
> INFO: \> Vendor: Apple
> INFO: \> Renderer: Apple M1 Pro
> INFO: \> Version: 4.1 Metal - 88.1
> INFO: \> GLSL: 4.10
> INFO: GL: VAO extension detected, VAO functions loaded successfully
> INFO: GL: NPOT textures extension detected, full NPOT textures supported
> INFO: GL: DXT compressed textures supported
> INFO: PLATFORM: DESKTOP (GLFW): Initialized successfully
> INFO: TEXTURE: \[ID 1\] Texture loaded successfully (1x1 | R8G8B8A8 | 1 mipmaps)
> INFO: TEXTURE: \[ID 1\] Default texture loaded successfully
> INFO: SHADER: \[ID 1\] Vertex shader compiled successfully
> INFO: SHADER: \[ID 2\] Fragment shader compiled successfully
> INFO: SHADER: \[ID 3\] Program shader loaded successfully
> INFO: SHADER: \[ID 3\] Default shader loaded successfully
> INFO: RLGL: Render batch vertex buffers loaded successfully in RAM (CPU)
> INFO: RLGL: Render batch vertex buffers loaded successfully in VRAM (GPU)
> INFO: RLGL: Default OpenGL state initialized successfully
> INFO: TEXTURE: \[ID 2\] Texture loaded successfully (128x128 | GRAY\_ALPHA | 1 mipmaps)
> INFO: FONT: Default font loaded successfully (224 glyphs)
> Please submit a bug report to https://github.com/modularml/mojo/issues and include the crash backtrace along with all the relevant source codes.
> Stack dump:
> 0. Program arguments: mojo test\_error.mojo
> #0 0x00000001046c8c24 llvm\_strlcpy (/Users/andresnowak/.modular/pkg/packages.modular.com\_mojo/bin/mojo+0x1000c0c24)
> #1 0x00000001046c6f10 llvm\_strlcpy (/Users/andresnowak/.modular/pkg/packages.modular.com\_mojo/bin/mojo+0x1000bef10)
> #2 0x00000001046c92c4 llvm\_strlcpy (/Users/andresnowak/.modular/pkg/packages.modular.com\_mojo/bin/mojo+0x1000c12c4)
> #3 0x0000000188473584 (/usr/lib/system/libsystem\_platform.dylib+0x180477584)
> #4 0x0000000300004120 
> #5 0x0000000300004120 
> #6 0x0000000104a5d530 \_\_jit\_debug\_register\_code (/Users/andresnowak/.modular/pkg/packages.modular.com\_mojo/bin/mojo+0x100455530)
> #7 0x000000010462956c \_mh\_execute\_header (/Users/andresnowak/.modular/pkg/packages.modular.com\_mojo/bin/mojo+0x10002156c)
> #8 0x0000000104628f60 \_mh\_execute\_header (/Users/andresnowak/.modular/pkg/packages.modular.com\_mojo/bin/mojo+0x100020f60)
> #9 0x0000000104611960 \_mh\_execute\_header (/Users/andresnowak/.modular/pkg/packages.modular.com\_mojo/bin/mojo+0x100009960)
> \#10 0x00000001880ba0e0 
> mojo crashed!
> Please file a bug report.
> \[53873:211975:20240628,151624.750679:WARNING crash\_report\_exception\_handler.cc:257\] UniversalExceptionRaise: (os/kern) failure (5)
> \[1\] 53871 bus error mojo test\_error.mojo
> \`\`\`
> \### linux output
> \`\`\`bash
> INFO: Initializing raylib 5.0
> INFO: Platform backend: DESKTOP (GLFW)
> INFO: Supported raylib modules:
> INFO: \> rcore:..... loaded (mandatory)
> INFO: \> rlgl:...... loaded (mandatory)
> INFO: \> rshapes:... loaded (optional)
> INFO: \> rtextures:. loaded (optional)
> INFO: \> rtext:..... loaded (optional)
> INFO: \> rmodels:... loaded (optional)
> INFO: \> raudio:.... loaded (optional)
> INFO: DISPLAY: Device initialized successfully
> INFO: \> Display size: 1920 x 1080
> INFO: \> Screen size: 800 x 600
> INFO: \> Render size: 800 x 600
> INFO: \> Viewport offsets: 0, 0
> INFO: GLAD: OpenGL extensions loaded successfully
> INFO: GL: Supported extensions count: 400
> INFO: GL: OpenGL device information:
> INFO: \> Vendor: NVIDIA Corporation
> INFO: \> Renderer: NVIDIA GeForce RTX 4070/PCIe/SSE2
> INFO: \> Version: 3.3.0 NVIDIA 550.67
> INFO: \> GLSL: 3.30 NVIDIA via Cg compiler
> INFO: GL: VAO extension detected, VAO functions loaded successfully
> INFO: GL: NPOT textures extension detected, full NPOT textures supported
> INFO: GL: DXT compressed textures supported
> INFO: GL: ETC2/EAC compressed textures supported
> INFO: PLATFORM: DESKTOP (GLFW): Initialized successfully
> INFO: TEXTURE: \[ID 1\] Texture loaded successfully (1x1 | R8G8B8A8 | 1 mipmaps)
> INFO: TEXTURE: \[ID 1\] Default texture loaded successfully
> INFO: SHADER: \[ID 1\] Vertex shader compiled successfully
> INFO: SHADER: \[ID 2\] Fragment shader compiled successfully
> INFO: SHADER: \[ID 3\] Program shader loaded successfully
> INFO: SHADER: \[ID 3\] Default shader loaded successfully
> INFO: RLGL: Render batch vertex buffers loaded successfully in RAM (CPU)
> INFO: RLGL: Render batch vertex buffers loaded successfully in VRAM (GPU)
> INFO: RLGL: Default OpenGL state initialized successfully
> INFO: TEXTURE: \[ID 2\] Texture loaded successfully (128x128 | GRAY\_ALPHA | 1 mipmaps)
> INFO: FONT: Default font loaded successfully (224 glyphs)
> INFO: FILEIO: \[resources/xbox.png\] File loaded successfully
> INFO: IMAGE: Data loaded successfully (800x450 | R8G8B8A8 | 1 mipmaps)
> INFO: TEXTURE: \[ID 3\] Texture loaded successfully (800x450 | R8G8B8A8 | 1 mipmaps)
> Texture(id: 3, width:800, height:450, mipmaps: 1, format: 7)
> \`\`\`
> 
> \### System information
> 
> \`\`\`shell
> \- What OS did you do install Mojo on ? MacOs and PopOs
> \- Provide version information for Mojo by pasting the output of \`mojo -v\` 24.4.0
> \- Provide Modular CLI version by pasting the output of \`modular -v\` 0.8.0
> \`\`\`

---

<div class="post-metadata">

**Author:** ![Hundo](https://avatars.discourse-cdn.com/v4/letter/h/c6cbf5/32.png) [@Hundo](https://forum.modular.com/u/Hundo)\
**Post date:** [April 13, 2026, 4:51am UTC](https://forum.modular.com/t/wgpu-mojo-wgpu-native-bindings/2957/4 "2026-04-13T04:51:31Z")

</div>

Thanks for the feedback. I’ll be honest—the origin and borrowing system in Mojo is still a significant learning curve for me. I’m currently digging into the stdlib and StringSpan to better understand how to implement this correctly.

The C callback implementation was indeed a bit of a naive placeholder. It was the most intuitive way for me to get a working prototype, but I realize now it needs a more native approach to handle Mojo’s requirements properly.

Regarding the issues already being tracked, I’m going to try reproducing them. I’m not entirely sure if I can isolate the ABI issues on my current setup, but I’ll jump into the GitHub thread to provide whatever logs or context I can find.

Appreciate the guidance as I navigate these lower-level details!

---

<div class="post-metadata">

**Author:** ![owenhilyard](https://sea1.discourse-cdn.com/flex001/user_avatar/forum.modular.com/owenhilyard/32/41_2.png) [@owenhilyard](https://forum.modular.com/u/owenhilyard)\
**Post date:** [April 13, 2026, 3:54pm UTC](https://forum.modular.com/t/wgpu-mojo-wgpu-native-bindings/2957/5 "2026-04-13T15:54:40Z")

</div>

> I’ll be honest—the origin and borrowing system in Mojo is still a significant learning curve for me.

You can treat it as a superset of how Rust does things, albeit with a requirement that you use `Pointer[T, origin]` instead of `&'origin T`. That should give you a lot more learning materials to work with. Sadly, origins currently aren’t explained super well and the biggest place you deal with all of the footguns is in FFI code like this, so you sort-of dove into the deep end here.

---

<div class="post-metadata">

**Author:** ![system](https://us1.discourse-cdn.com/flex001/uploads/modular/original/1X/2751e0fbdc595a99718b216730957e9db4448cfd.jpeg) [@system](https://forum.modular.com/u/system)\
**Post date:** [October 10, 2026, 3:55pm UTC](https://forum.modular.com/t/wgpu-mojo-wgpu-native-bindings/2957/6 "2026-10-10T15:55:26Z")

</div>

Closed on October 10, 2026, 3:55pm UTC

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.
