Zig bindings for wgpu-native
Requires Zig 0.16.x.
This package exposes two modules: wgpu-c and wgpu.
wgpu-c is wgpu.h (and therefore webgpu.h) translated directly by Zig, so it
tracks wgpu-native's C API without a second handwritten declaration layer.
wgpu is a Zig-friendly wrapper over the bindings generated directly from wgpu.h and
webgpu.h. The generated declarations are also available through wgpu.raw; wrapper
methods and raw calls therefore share the headers as their single ABI source of truth.
The pinned wgpu-native v29.0.0.0 headers currently declare 226 functions. The
wgpu wrapper exposes every function with a usable v29 implementation, while
wgpu.raw and wgpu-c expose all function and type declarations produced by
translate-c. A compile-time audit checks this partition, rejects handwritten
extern fn wgpu... declarations, and fails when a future header update adds an
unclassified function or type.
The following v29 API groups intentionally remain raw-only because their upstream implementations panic, are blocked, or always return an unavailable result:
wgpuGetProcAddressand the currently unimplemented*SetLabelfunctions.- Async pipeline creation, shader compilation info, device-lost futures,
waitAny, WGSL-language feature queries, and global instance-feature enumeration. - Buffer map-state and mapped-range copy helpers.
- External-texture lifecycle functions.
- Device adapter-info lookup and texture binding-view-dimension lookup.
- The native Metal command-queue accessor, which always returns null in v29.
Supported wgpu-native extensions are available as regular Zig methods, including
Queue.getTimestampPeriod, graphics-debugger capture control, and the borrowed Metal
device/texture accessors. Platform-native pointers are optional and must not be released
by the caller.
Add the package to your dependencies, either with:
zig fetch --save https://github.com/openharmony-zig/wgpu_native_zig/archive/refs/tags/v7.0.0.tar.gzor by manually adding to your build.zig.zon:
.{
// ...other stuff
.dependencies= .{
// ...other dependencies
.wgpu_native_zig= .{
// You can either use a commit hash:
.url="https://github.com/openharmony-zig/wgpu_native_zig/archive/<commit_hash>.tar.gz",
// or a tagged release:// .url = "https://github.com/openharmony-zig/wgpu_native_zig/archive/refs/tags/v7.0.0.tar.gz`
.hash="<dependency hash>"
}
}
}Then, in build.zig add:
constwgpu_native_dep=b.dependency("wgpu_native_zig", .{});
// Add module to your exe (wgpu-c can also be added like this, just pass in "wgpu-c" instead)exe.root_module.addImport("wgpu", wgpu_native_dep.module("wgpu"));
// Or, add to your lib similarly:lib.root_module.addImport("wgpu", wgpu_native_dep.module("wgpu"));Windows x86_64 has two options for ABI: GNU and MSVC. For i686 and aarch64, only the MSVC option is available. If you need to specify the build target, you can do that with:
consttarget=b.standardTargetOptions(.{
.default_target= .{
// If not specified, defaults to the GNU abi
.abi=.msvc,
}
});Or, specify it with your build command. For example, the triangle example in this repository can be run like so:
zig build --build-file build.examples.zig run-triangle-example -Dtarget=x86_64-windows-msvcEither way, pass the resolved target to the dependency like so:
constwgpu_native_dep=b.dependency("wgpu_native_zig", .{
.target=target
});When using static linking with MSVC, you might encounter duplicate symbol errors. If so, try
if (target.result.abi==.msvc) {
// "exe" here is the *std.Build.Step.Compile from b.addExecutable() (or b.addTest())exe.bundle_compiler_rt=false;
exe.bundle_ubsan_rt=false;
}An example of using wgpu-native-zig with static linking on Windows can be found at wgpu-native-zig-windows-test.
Dynamic linking can be made to work, though it is a bit messy to use.
When you initialize your wgpu_native_dep, add the option for dynamic linking like so:
constwgpu_native_dep=b.dependency("wgpu_native_zig", .{
// Defaults to .static if you don't specify
.link_mode=.dynamic
});Then add the following with your install step dependencies:
constlib_dir=wgpu_native_dep.namedWriteFiles("lib").getDirectory();
// This would also work with .so files on linuxconstdll_path=lib_dir.join(b.allocator, "wgpu_native.dll") catchreturn;
// addInstallBinFile puts the dll in the same directory as your executableconstinstall_dll=b.addInstallBinFile(dll_path, "wgpu_native.dll");
// Make sure that the dll is installed when the install step is runb.getInstallStep().dependOn(&install_dll.step);wgpu-native v29.0.0.0 is built from its pinned source commit by default. The matching
webgpu-headers commit is pinned separately, so the generated C ABI does not drift when
an upstream branch changes.
The source build requires Cargo, a Rust toolchain with the selected target installed, and the platform SDK normally required by that target. For example:
# Native source build. Installs the static library, dynamic library, and C headers.
zig build -Doptimize=ReleaseFast
# Compile the bindings and link probes without running them.
zig build --build-file build.tests.zig check -Doptimize=ReleaseFastTargets that cannot legally be built on every host still require their native toolchain: iOS requires Xcode on macOS, MSVC requires Windows, and Android/OpenHarmony require their respective NDKs.
The root build.zig only builds wgpu-native and exposes the wgpu/wgpu-c binding
modules. build/Library.zig is the shared library entry point, while
build/platform/root.zig dispatches to the Android, Apple, Linux, OpenHarmony, or
Windows build implementation. Tests and examples are isolated behind
build.tests.zig and build.examples.zig; their implementation stays in the
corresponding directory.
Set WGPU_NATIVE_USE_PREBUILT=1 to skip the Cargo source build and use the published
wgpu-native archive for the selected target:
WGPU_NATIVE_USE_PREBUILT=1 zig build --build-file build.tests.zig check \
-Dtarget=x86_64-linux-gnuThe equivalent Zig build option is -Duse_prebuilt=true. Downstream packages can pass it
while resolving this dependency:
constwgpu_native_dep=b.dependency("wgpu_native_zig", .{
.target=target,
.optimize=optimize,
.use_prebuilt=true,
});WGPU_NATIVE_PREBUILT_DIR=/path/to/prefix uses a local artifact directory and also
implies prebuilt mode. The prefix must use the layout produced by this package:
prefix/
├── include/webgpu/{webgpu.h,wgpu.h}
└── lib/
├── libwgpu_native.a
└── libwgpu_native.so
Use the platform-specific dynamic and import-library names on Apple and Windows.
OpenHarmony currently uses this local-directory mechanism when consuming CI artifacts,
because upstream wgpu-native does not publish OpenHarmony archives.
| Platform | Architectures / ABIs | Source build | Published prebuilt |
|---|---|---|---|
| Android | arm64-v8a, armeabi-v7a, x86, x86_64 | Yes, with ANDROID_NDK_HOME | Yes |
| iOS | arm64 device, arm64 simulator, x86_64 simulator | Yes, on macOS | Yes |
| Linux | aarch64, x86_64 (GNU); aarch64, x86_64 (musl) | Yes | GNU targets |
| macOS | aarch64, x86_64 | Yes, on macOS | Yes |
| Windows | aarch64/x86/x86_64 MSVC, x86/x86_64 GNU | Yes, on Windows | All except x86 GNU |
| OpenHarmony | arm64-v8a, armeabi-v7a, x86_64 | Yes, with OHOS_NDK_HOME | Local CI artifact |
The OpenHarmony commands are:
rustup target add \
aarch64-unknown-linux-ohos \
armv7-unknown-linux-ohos \
x86_64-unknown-linux-ohos
zig build --build-file build.tests.zig check \
-Dtarget=aarch64-linux-ohos -Doptimize=ReleaseFast
zig build --build-file build.tests.zig check \
-Dtarget=arm-linux-ohoseabi -Doptimize=ReleaseFast
zig build --build-file build.tests.zig check \
-Dtarget=x86_64-linux-ohos -Doptimize=ReleaseFastThe target-artifact workflow builds the complete matrix on Linux x86_64/aarch64, macOS
arm64/Intel, Windows, Android, and OpenHarmony runners. Every target runs the
binding/ABI audit before packaging. Desktop targets compile all wrapper tests, while
Android, OpenHarmony, and arm64 iOS targets build a Zig link probe. Zig 0.16 cannot link
the historical x86_64-apple-ios Mach-O platform marker as an explicit simulator dylib,
so that one target runs the binding/ABI audit without the link probe. Every artifact
prefix contains both link modes and the matching headers.
- Names are shortened to remove redundancy.
- For example
wgpu.WGPUSurfaceDescriptorbecomeswgpu.SurfaceDescriptor
- For example
- C pointers (
[*c]) are replaced with more specific pointer types.- For example
[*c]const u8is replaced with?[*:0]const u8.
- For example
- Pointers to opaque structs are made explicit (and only optional when they need to be).
- For example
wgpu.WGPUAdapterfromwebgpu.hwould instead be expressed as*wgpu.Adapteror?*wgpu.Adapter, depending on the context.
- For example
- Methods are expressed as decls inside of structs
- For example
becomes
wgpu.wgpuInstanceCreateSurface(instance: WGPUInstance, descriptor: [*c]constWGPUSurfaceDescriptor) WGPUSurface
Instance.createSurface(self: *Instance, descriptor: *constSurfaceDescriptor) ?*Surface
- For example
- Callback-based operations provide synchronous helpers for adapter/device requests,
buffer mapping, submitted queue work, and device error scopes.
- For example, requesting an adapter with a callback looks something like
whereas the non-callback version looks like
fnhandleRequestAdapter( status: Instance.RequestAdapterStatus, adapter: ?*Adapter, message: StringView, userdata1: ?*anyopaque, userdata2: ?*anyopaque ) callconv(.c) void { switch(status) { .success=> { constud_adapter: **Adapter=@ptrCast(@alignCast(userdata1)); ud_adapter.*=adapter.?; }, else=> { std.log.err("{s}\n", .{message.toSlice()}); } } constcompleted: *bool=@ptrCast(@alignCast(userdata2)); completed.*=true; } varadapter_ptr: ?*Adapter=null; varcompleted=false; constrequest_adapter_info=Instance.RequestAdapterCallbackInfo { .callback=handleRequestAdapter, .userdata1=@ptrCast(&adapter_ptr), .userdata2=@ptrCast(&completed), } constra_future=instance.requestAdapter(null, request_adapter_info); // wgpu-native v29 does not implement Instance.waitAny(), so drive// allow_process_events callbacks with Instance.processEvents()._=ra_future; instance.processEvents(); while(!completed) { tryio.sleep(.fromNanoseconds(200_000_000), .awake); instance.processEvents(); }
The synchronous responses own their copied callback messages and any returned handle. Call// The wrapper methods use polling, so 200_000_000 is the polling interval in nanoseconds.varresponse=tryinstance.requestAdapterSync(allocator, io, null, 200_000_000); deferresponse.deinit(allocator); constadapter_ptr: ?*Adapter=switch (response.status) { .success=>response.takeAdapter(), else=>blk: { std.log.err("{s}\n", .{response.messageorelse"adapter request failed"}); break :blknull; } };
deinit()on every response, and usetakeAdapter()ortakeDevice()to transfer a successful handle out of it. The other helpers follow the same lifetime model:Buffer.mapSync(),Queue.onSubmittedWorkDoneSync(), andDevice.popErrorScopeSync(). If the suppliedIois cancelled, the synchronous wrapper finishes draining the native callback before returning the cancellation error, yielding the polling thread betweenprocessEvents()calls. This is required because wgpu-native v29 has nowaitAny()implementation and the callback userdata must remain alive until completion.
- For example, requesting an adapter with a callback looks something like
- Output structures whose members are allocated by wgpu-native use pointer-based
deinit()methods. In particular,SupportedFeatures,AdapterInfo, andSurfaceCapabilitiesclear their owned pointers, counts, and strings after releasing them, so a deferreddeinit()cannot observe stale members.SupportedFeatures.slice()and theSurfaceCapabilities.*Slice()accessors expose their pointer/count arrays as bounded read-only slices. Instance.enumerateAdapters()allocates anAdapterListthat owns every returned adapter. Deinitialize the list to release all remaining adapters, or transfer one:varadapters=tryinstance.enumerateAdapters(allocator, null); deferadapters.deinit(allocator); constadapter=adapters.takeAdapter(0).?; deferadapter.release();
SurfaceTextureowns the texture returned bySurface.getCurrentTexture(). Usedefer surface_texture.deinit()to release it automatically, or calltakeTexture()to transfer the texture to code that will release it.- Wrapper methods use slices where the C API uses a pointer/count pair.
Queue.writeBuffer()andQueue.writeTexture()take byte slices,setBindGroup()takes a dynamic-offset slice, andsetImmediates()takes a byte slice. Buffer mapped-range accessors return bounded byte slices and resolveWGPU_WHOLE_MAP_SIZEagainst the buffer size. - ABI-compatible descriptors retain their C pointer/count fields, while
init()and chainablewithXxx()helpers accept borrowed slices and synchronize both fields. The slice must remain alive until the native call returns. - Chained structs are provided with inline functions for constructing them, which come in two forms depending on whether or not the chained struct is likely to always be required.
- For required chained structs, you can either write them explicitely:
or use a function to construct the root descriptor:
constsource=SurfaceSourceXlibWindow{ .display=display, .window=window, }; constdescriptor=SurfaceDescriptor{ .next_in_chain=&source.chain, .label=StringView.fromSlice("xlib_surface_descriptor"), };
constsource=SurfaceSourceXlibWindow{ .display=display, .window=window, }; constdescriptor=surfaceDescriptorFromXlibWindow( &source, "xlib_surface_descriptor", );
- For optional chained structs, create the extension separately and attach it
with
withExtras():The source or extras value must remain alive until the native API call that consumes the descriptor has returned.constextras=SurfaceConfigurationExtras{ .desired_maximum_frame_latency=2, }; constconfiguration= (SurfaceConfiguration{ .device=device, // other fields }).withExtras(&extras);
- For required chained structs, you can either write them explicitely:
WGPUBoolis replaced withboolwhenever possible.- This means it is replaced with
boolin wrapper method parameters and return values, but not in structs that preserve the C ABI.
- This means it is replaced with
- Callback types that belong to one handle are scoped under that handle. For example,
use
Instance.RequestAdapterCallbackInfo,Adapter.RequestDeviceCallbackInfo,Buffer.MapCallbackInfo, andDevice.PopErrorScopeCallbackInfo. - Wrapper names retain meaningful WebGPU prefixes. For example,
WGPUTextureSampleTypeis exposed asTextureSampleType.
- Expand headless coverage for limit queries, textures, samplers, query sets, and render bundles.
- Port wgpu-native-examples using wrapper code, as a basic form of documentation.