{"slug": "dassdl3-idiomatic-sdl3-bindings-for-daslang", "title": "dasSDL3: Idiomatic SDL3 bindings for daslang", "summary": "Developer GaijinEntertainment released dasSDL3, a set of idiomatic SDL3 bindings for the daslang programming language, generated with the dasClangBind tool and layered with an sdl_boost helper library. The bindings cover all of SDL3's functionality across several platforms, including its abstraction over modern GPU APIs such as DirectX, Metal, OpenGL, and Vulkan, and add daslang-specific idioms including the |> pipeline operator, builder-style object construction, and named initialization. The interface design was modeled on the Rust SDL3 bindings.", "body_md": "*This is an AI-assisted English translation of the original post: [dasSDL3 (Russian original)](https://spiiin.github.io/blog/3531085095/).*\n\nI made [SDL](https://www.libsdl.org/) bindings for [daslang](https://daslang.io/).\n\n# SDL3[#](#sdl3)\n\nSDL abstracts access to hardware and operating system facilities. Version 3 also introduced an abstraction over [modern GPU APIs](https://wiki.libsdl.org/SDL3/CategoryGPU). You can still use SDL just to create a window and draw into it through another graphics API: DirectX, Metal, OpenGL, or Vulkan. There are also several [companion libraries](https://wiki.libsdl.org/SDL3/Libraries) that extend SDL with image loading, higher-level audio and networking APIs, and simple 2D graphics.\n\nI wanted this kind of Swiss Army knife for `daslang`, so I used AI to build bindings for all of SDL3’s functionality across several platforms.\n\n# Automatic binding generation[#](#automatic-binding-generation)\n\nThe bindings are generated with [dasClangBind](https://github.com/GaijinEntertainment/daScript/tree/master/modules/dasClangBind), which works with a subset of C++. It parses the library’s code and generates basic bindings. These still need some polishing: removing helper macros and functions that do not belong in the bindings, and adapting idioms that do not translate well between languages, such as raw pointers and objects with different lifetimes or memory management rules. Getting working bindings is the first and easiest part of the job.\n\n# Idiomatic APIs[#](#idiomatic-apis)\n\nThe next layer is `sdl_boost`, a set of helpers on top of the basic bindings that makes the library easier to use and the code more expressive. Every language has its own idioms. Good bindings let you use library functions in forms that feel natural in the target language. The syntax macros in `daslang` are a great fit for this.\n\nI used the [Rust SDL3 bindings](https://docs.rs/sdl3/latest/sdl3/index.html) as a reference when designing the interface. I have already written about the Rust community’s approach to API design: [Elegant APIs in Rust](https://spiiin.github.io/blog/3198586047/). Here I adapted those ideas to `daslang`.\n\n## Pipelines[#](#pipelines)\n\nPipelines are one of the features that make an interface convenient to use. See [Pipelining might be my favorite programming language feature](https://herecomesthemoon.net/2025/04/pipelining/). In `daslang`, the `|>` operator makes `value |> function(argument)` equivalent to `function(value, argument)`. The result of one call becomes the first argument of the next, so the code reads in execution order.\n\n## Builder[#](#builder)\n\nYou can build up an object description step by step. For example, set a window’s size, add a flag, and choose its position:\n\n`window_options` creates a `WindowOptions`, and each subsequent call returns an updated description. The window does not exist yet: you can prepare the settings separately and then pass them to `with_window`. Similar chains are available for textures, shaders, samplers, and graphics pipelines.\n\nFor example, here is a sampler description with linear filtering, interpolation between mip levels, and texture wrapping:\n\n`filters` sets the minification and magnification filters, `mipmap_mode` controls filtering between mip levels, and `address_modes` determines how coordinates outside the texture are handled. The result is an ordinary `SDL_GPUSamplerCreateInfo`, which you can pass to `with_gpu_sampler(device, sampler_settings)` to create a GPU resource with a defined lifetime.\n\nThese are ordinary free functions. You do not need to turn the description into a class with methods to get a chain of calls. The parentheses around the multiline expression allow continuation lines starting with `|>`.\n\n## Named initialization[#](#named-initialization)\n\nFor short descriptions, it is convenient to specify the fields directly:\n\nA builder is useful when you assemble a description step by step; named initialization is useful when all the settings are known up front.\n\n## Return values and Result[#](#return-values-and-result)\n\nIn C APIs, results are often written to parameters passed by pointer. The helper layer can return them as ordinary values. For example, `window_size(window)` returns a `Result<int2, SdlError>` containing either the window size or an error description. In `daslang` syntax, this type is written as `$Result<int2; SdlError>`.\n\nLong type names can be shortened with `typedef`. For example, the library defines this type for operations without a meaningful success value:\n\nFunction signatures can now use `SdlStatus` instead of the full type. `SdlUnit` represents an empty success value, and `sdl_ok()` creates that successful result. `SdlError` stores the operation name and the error message, copied before resources are released.\n\nYou can also introduce aliases for a specific task:\n\nFor results with different value types, the library provides the generic form `$SdlResult<T>`. For example, `$SdlResult<int2>` is the same type as `$Result<int2; SdlError>`. It is implemented by a type macro that supplies `SdlError` to the standard `Result`. These abbreviations give types convenient names while preserving their representation and behavior.\n\n`Option` represents an absent value. For example, an SDL hint may not be set, in which case you can supply a fallback:\n\nThe interface thus distinguishes an operation failure from the normal absence of a value.\n\n## Early returns with sdl_try[#](#early-returns-with-sdl-try)\n\nWhen several operations return `Result`, you have to check for an error after each one. For example, let’s get the window size, print it, and clear the renderer. Without any helper syntax, the code looks like this:\n\n`window_size` and the enclosing function return results with different success types: `int2` and `SdlUnit`. If the first call fails, its `SdlError` must be placed into a result with the appropriate success type. For `clear`, the result can be returned directly.\n\nWith `sdl_try`, the same function becomes shorter:\n\n`sdl_try` is a syntax macro: on success, it extracts the value; on failure, it returns early from the current function or block, preserving the `SdlError`. The checks from the first example are still there, but the macro generates them. The calls read as a sequence of actions, and error reporting can be left to the application boundary.\n\nThe enclosing function or block must return a `Result` whose error type is `SdlError`. `sdl_try` does not unwrap an `Option` or manage pointer lifetimes; resources use `with_*` scopes.\n\n## The same idea in other languages[#](#the-same-idea-in-other-languages)\n\nIn `daslang`, the idiom is implemented at the library level: `sdl_try` generates checks and early returns through a syntax macro. Explicitly marking potential exit points seemed the most convenient approach to me: it shows where execution may end while keeping the code linear, without nested blocks or extra braces. Other languages have similar mechanisms for stopping a chain when a value is absent.\n\nThe examples below use a fictional SDL API: first we create a window, then a renderer for it. The creation functions return `Option`/` Maybe`; if either step produces no value, the remaining steps are skipped.\n\n**Rust:** the [`?`](https://doc.rust-lang.org/std/option/index.html#the-question-mark-operator) operator extracts a value from `Some`, or returns `None` from the current function when it encounters `None`. The potential exit points are visible in the expressions:\n\n**Haskell:** in a `do` block for `Maybe`, `<-` extracts a value from `Just`. On `Nothing`, the whole block evaluates to `Nothing` and the continuation is skipped. This behavior comes from [binding computations for Maybe](https://downloads.haskell.org/~ghc/9.0.1-alpha1/docs/html/libraries/base-4.15.0.0/src/GHC-Base.html): the point where the chain stops lies “between the lines,” without a separate exit operator.\n\n## Resource lifetimes through blocks[#](#resource-lifetimes-through-blocks)\n\nIn C++, RAII is the usual approach to resource management: an owning object acquires a resource when constructed and releases it in its destructor when it leaves scope. This model of owning objects is less typical in `daslang`: explicit blocks and deferred cleanup through `defer` are convenient ways to define the lifetimes of external resources. The language has finalizers and `inscope`, but a pointer to an SDL object alone does not define its ownership or cleanup rules.\n\nCreating a window or texture is only half the job: the resource must be released, including on an early return caused by an error. The `with_*` functions pass a resource to a block and release it when the block finishes. `sdl_scope` and `sdl_use` let you write several nested blocks as a linear sequence:\n\nThis example draws one frame; an application needs an event and rendering loop inside the resource lifetime. When the block ends, the renderer is released first, then the window, and finally SDL is shut down. If renderer creation or drawing fails, resources that have already been created are also released.\n\nThe `sdl_use` macro moves the rest of the block into the callback of the corresponding `with_*` function. The pointers remain borrowed: they can be used inside that scope, but must not be saved for later use or released manually. Resource lifetimes follow the structure of the program; no separate resource collector is needed.\n\nFor comparison, here is the same example without `with_*` and `sdl_use` (expand `sdl_try` as well, and it looks almost like C). Each resource is created before entering its cleanup block. `defer` is moved to the finalization section of its entire block, so placing it after resource creation in the same block is not enough: cleanup could also run on an early return before creation succeeds.\n\nIf renderer creation fails, the window and SDL are cleaned up. If drawing fails, all three resources are released in reverse order. `with_*` encapsulates these blocks and cleanup rules, while `sdl_use` lets you use them without writing the nesting by hand.\n\n## Events as a sequence[#](#events-as-a-sequence)\n\nYou can process the event queue with an ordinary loop:\n\n`poll_events()` is a lazy iterator: it retrieves one event at a time and stops when the queue is empty. If you leave the loop early, subsequent events remain queued. Instead of a raw `SDL_Event` containing a C union, the code receives an `SdlEvent`, a variant type containing decoded event data. Strings and lists in that data belong to the resulting value, so the next poll will not overwrite them.\n\n## Pattern matching on events[#](#pattern-matching-on-events)\n\nThe `SdlEvent` variant type can be inspected with `match`. Each branch receives the data for its corresponding event:\n\n## Borrowed memory access[#](#borrowed-memory-access)\n\nPixel operations use another form of the same block idiom: the library temporarily provides access to texture memory. For example, let’s fill a 32 × 32 RGBA32 streaming texture with a gradient:\n\n`with_texture_pixels_rgba8` locks the texture while its block runs, and `with_row` provides a borrowed array of pixels from one row. Here, `#` marks temporary borrowed access: this data cannot be retained or passed out of the block. `rgba8` packs the components into a single `uint`. The code operates directly on texture memory, while the library accounts for row pitch and unlocks the texture when the block finishes, including on an error return. The wrapper also defines the data type, avoiding unsafe access through `void*` pointers.\n\n## Different types for different resources[#](#different-types-for-different-resources)\n\nAn ordinary `typedef` shortens a type name without separating it from the original type. The checked GPU API needs genuinely different types: buffers, textures, and samplers must not accidentally replace each other. Their handles are therefore registered as `distinct` types. Their definitions are equivalent to the following; you should not redeclare them in your script:\n\nAll three have the same machine representation, but the compiler treats them as different types. For example, vertex binding takes an array specifically of `GpuBufferHandle`:\n\nPassing a `GpuTextureHandle` in place of `buffer` is a compile-time error. At runtime, the checked API also validates the resource kind, whether it still exists, and which device it belongs to. A copied handle remains an alias to the same resource: it does not create separate ownership or extend its lifetime. These checks apply to the checked GPU API; direct SDL calls using native pointers retain their original contracts.\n\n## Arrays instead of pointers and counts[#](#arrays-instead-of-pointers-and-counts)\n\nMany C functions accept a data pointer and a separate element count. An array is more convenient for scripts because its size is already known. For example, let’s draw a triangle:\n\nThe adapter passes the pointers and counts to SDL itself. Before the call, it checks index bounds, supported array sizes, and vertex values.\n\n## Temporary state changes[#](#temporary-state-changes)\n\nA block can also define how long a setting applies. For example, `with_render_target` saves the current render target, switches to a texture, and restores the previous target when the block finishes:\n\n## Partial results together with status[#](#partial-results-together-with-status)\n\nFor IO, a `Result<array<uint8>, SdlError>` may be insufficient: an operation might transfer some data and then fail. Therefore, `read_io` and `write_io` return `IoTransfer`, which stores the transferred byte count separately from the status:\n\n`transferred` is available even when `status` contains an error.\n\n# Shader DSL[#](#shader-dsl)\n\nAnother feature is writing shaders in `daslang`. This uses the existing dasSpirv compiler: annotations mark shader functions, and the compiler generates SPIR-V and reflection metadata while compiling the script. The SDL layer uses those results to create GPU resources.\n\nThe chain looks like this: annotated function → SPIR-V and reflection → resource layout validation → SDL GPU shader creation. The shader is compiled when the script is compiled, while the GPU object is created at runtime, once a device is available.\n\n## Describing a shader[#](#describing-a-shader)\n\nFor example, here is a fragment shader that reads its color from a uniform block:\n\n`@uniform` describes data supplied to the shader by the application, while `@out` describes its output.\n\nThe annotation produces two arrays: `solid_fragment : array<uint>` containing SPIR-V and `solid_fragment_reflect : array<uint>` containing reflection. Reflection describes the shader stage and the resources it uses. The source function is named `fragment_main`, but the generated SPIR-V entry point is named `main`.\n\n## Creating an SDL GPU shader[#](#creating-an-sdl-gpu-shader)\n\nOnce a device is available, both arrays are passed to `with_gpu_dsl_shader`. This fragment uses the definitions from the previous example:\n\nThe wrapper reads reflection, validates the resources against SDL conventions, and fills in `SDL_GPUShaderCreateInfo`: the stage and the number of uniform blocks and samplers. SPIR-V is converted from an array of words to an array of bytes and passed to the regular shader creation function. The resulting object’s lifetime is managed by the familiar `with_*` scope.\n\nCode and reflection must come from the same compilation. Reflection helps fill in creation parameters, but the application still controls compatibility between vertex and fragment shaders, data formats, and graphics pipeline configuration.\n\nYou can also use precompiled shaders, skipping the compilation stage.\n\n## Passing uniform structures[#](#passing-uniform-structures)\n\nThe `Tint` structure can also be used on the application side. However, its ordinary memory representation cannot be uploaded directly: the GPU expects std140 layout. A packing adapter handles this:\n\nThe application’s structure must match the shader declaration: the wrapper does not determine the active shader from the command buffer. To pack repeatedly without allocating a new temporary buffer, use `pack_gpu_dsl_uniform` with a reusable byte array.\n\n## Compute and graphics backends[#](#compute-and-graphics-backends)\n\nThe same mechanism works for compute shaders: `[compute_shader]` generates SPIR-V and reflection, and `with_gpu_dsl_compute_pipeline` creates an SDL compute pipeline, deriving workgroup dimensions and resource counts from reflection. For storage resources, the additional `sdl_shader_access` annotation records read and write access modes, while std430 adapters pack arrays of structures into storage buffers.\n\nThe direct SPIR-V path is used for Vulkan. D3D12 has a separate SDL_shadercross integration: `with_gpu_dsl_shader_cross` and `with_gpu_dsl_compute_pipeline_cross` translate the same SPIR-V into the format required by the backend. This path requires shadercross and the corresponding compiler dependencies.\n\nArticles about shaders in SDL:[https://moonside.games/posts/introducing-sdl-shadercross/](https://moonside.games/posts/introducing-sdl-shadercross/)[https://moonside.games/posts/layers-all-the-way-down/](https://moonside.games/posts/layers-all-the-way-down/)\n\n# Language and ecosystem integration[#](#language-and-ecosystem-integration)\n\n## JIT/AOT[#](#jit-aot)\n\n`daslang` is more than a scripting language. On supported platforms, its JIT compilation modes often make interpreted code several times faster. Where JIT is unavailable, it can transpile code to C++. This is also supported and covered by tests to prevent regressions.\n\n## Live mode[#](#live-mode)\n\nThe language also supports live reload. You can start an application with an empty window and keep adding functionality without restarting it. The mechanism is described in [Running it live](https://daslang.io/blog/running-it-live.html).\n\nIn the SDL examples, the window, renderer, and ImGui context belong to the native host and survive script reloads. The `live_watch_boost` module watches for file changes and requests a reload after a save. Values annotated with `@live` are restored during incremental reloads; a full reload resets the script’s state.\n\nExamples on GitHub:\n\n- [01_widgets.das](https://github.com/spiiin/dasSDL3/blob/main/examples/live/01_widgets.das) — an SDL/ImGui application with automatic reload and control through stdin/stdout.\n- [02_widgets_http.das](https://github.com/spiiin/dasSDL3/blob/main/examples/live/02_widgets_http.das) — the same application controlled through a local HTTP API, with support for connecting an MCP client.\n- [03_widgets_recording.das](https://github.com/spiiin/dasSDL3/blob/main/examples/live/03_widgets_recording.das) — a version that records frames to APNG.\n\nA minimal live interface fragment:\n\n## Tests and automated tutorials[#](#tests-and-automated-tutorials)\n\nUI automation integrates with `imgui_playwright` from daslang. It provides a scripting API for ImGui applications: widgets are addressed by names such as `MAIN/INCREMENT`, and you can take snapshots, click or drag, wait for a value to change, and request a reload.\n\nFor example, after connecting `app` to the HTTP example, you can check that a click worked and its result survived a reload:\n\nThe complete [playwright_widgets.das](https://github.com/spiiin/dasSDL3/blob/main/examples/live/playwright_widgets.das) also drags a slider through synthetic mouse events and checks its value after a reload. The [automated test](https://github.com/spiiin/dasSDL3/blob/main/tests/test_live_mcp.py) additionally compares UI pixels to verify changes in rendering.\n\nThe same scenario can record a demonstration or tutorial. [record_widgets.das](https://github.com/spiiin/dasSDL3/blob/main/examples/live/record_widgets.das) runs a sequence of actions inside `with_recording_app`: it pauses, moves a slider, clicks a button, and checks the result. The application captures frames through SDL, and dasStbImage writes them to APNG. On Windows, the build with recording support can be launched with one command from the repository root:\n\nThis makes tutorial actions reproducible after UI changes. The scenario both describes the demonstration and checks that its actions produce the expected results. AI agents are good at using this interface.\n\n## daspkg integration[#](#daspkg-integration)\n\nThe library can be installed as a ready-to-use package through [daspkg](https://daslang.io/daspkg.html), without generating bindings or building it yourself. The source distribution also includes generated bindings, so you do not need to bring in LLVM and Clang.\n\n# Platforms and graphics backends[#](#platforms-and-graphics-backends)\n\nThe library has separate profiles for Windows, Linux, macOS, and the browser. Shared boost modules sit on top of bindings that account for each platform’s ABI and available functions.\n\n| Platform | Supported features | \n|---|---|\n| Windows x64 | Native MSVC build, interpreter and AOT; GPU examples for Vulkan and Direct3D 12. | \n| Linux | Core profile with GCC, interpreter and strict AOT; 2D examples on Ubuntu/WSL2 through WSLg. GPU rendering requires a working Vulkan device. | \n| macOS | Native Apple Clang build, Cocoa/Metal, interpreter and strict AOT; Metal tests include rendering and readback. | \n| Browser | Emscripten build targeting WebAssembly: SDL Renderer through WebGL, input and audio. There is also a separate standalone wasm32 AOT example. | \n\nGraphics uses two paths. `SDL_Renderer` provides ready-made 2D operations for textures, rectangles, and geometry. `SDL_GPU` gives you control over shaders, buffers, graphics pipelines, and compute pipelines. The browser profile uses Renderer/WebGL; the native SDL GPU examples have not yet been ported to it, and the pinned SDL version has no WebGPU backend.\n\nShader format also matters for SDL GPU. Vulkan accepts SPIR-V, Direct3D 12 accepts DXIL, and Metal accepts MSL or Metallib. In the GPU examples, a single `daslang` source is compiled to SPIR-V: Vulkan uses it directly, while Direct3D 12 and Metal use SDL_shadercross. The Direct3D 12 path also needs DXC. You can also supply precompiled shaders in the appropriate format.\n\nThe backend can be selected through `SDL_GPU_DRIVER`: `vulkan`, `direct3d12`, or `metal`.\n\nSDL can also be used with the separate Vulkan and OpenGL bindings available in `daslang` through dasVulkan and dasOpenGL. SDL then handles the window, input, and platform integration, while the application calls the graphics API directly.\n\nFor **`OpenGL`**, create a window with `SDL_WINDOW_OPENGL` and a context through `with_gl_context`. Once the context is current, you can use `require opengl` and ordinary `glViewport`, `glClear`, drawing, and resource upload calls. Present the frame with `SDL_GL_SwapWindow`. There is a [context creation example](https://github.com/spiiin/dasSDL3/blob/main/examples/89_gl_context.das) and [dasOpenGL rendering examples](https://github.com/spiiin/dasSDL3/tree/main/examples/web/opengl), which use OpenGL ES/WebGL 2 in the browser.\n\nFor **`Vulkan`**, SDL creates a window with `SDL_WINDOW_VULKAN`, reports the required instance extensions through `vulkan_instance_extensions`, and creates a window surface through `with_vulkan_surface`. The instance itself is created through Vulkan with those extensions enabled; devices, queues, swapchains, and rendering commands also remain the responsibility of Vulkan code. dasVulkan provides access to this API from `daslang`, while the SDL wrapper handles the platform-specific window and surface work. The [extension query example](https://github.com/spiiin/dasSDL3/blob/main/examples/90_vulkan_extensions.das) and [Vulkan interop contracts](https://github.com/spiiin/dasSDL3/blob/main/docs/vulkan-metal.md) illustrate this boundary. This path requires a Vulkan module or a custom native host; the standard SDL runner does not include it by itself.\n\nPortability is checked through tests and [GitHub CI](https://github.com/spiiin/dasSDL3/tree/main/.github/workflows) for Windows, Linux, and macOS. Build, API, and AOT checks are supplemented by separate graphics and browser test runs. Otherwise, updating or adding features would become a nightmare.\n\n# Examples[#](#examples)\n\nTo test the API, I ported several BGFX examples; they also make a useful performance reference:[bgfx examples ported to daslang and SDL GPU](https://github.com/spiiin/dasSDL3/tree/main/examples/gpu). The shaders in these examples are also written in `daslang`.\n\nThe latest port is [Shadow volumes](https://github.com/spiiin/dasSDL3/blob/main/examples/gpu/10_shadowvolumes.das): a scene with shadow volumes and several light sources. Here is a screenshot of the example running on the Vulkan backend:\n\nFor the web version, I also made an NES emulator that runs in the browser:[https://github.com/spiiin/dasNES](https://github.com/spiiin/dasNES)[https://spiiin.github.io/dasNES/](https://spiiin.github.io/dasNES/)", "url": "https://wpnews.pro/news/dassdl3-idiomatic-sdl3-bindings-for-daslang", "canonical_source": "https://spiiin.github.io/blog/339855472/", "published_at": "2026-10-10 13:48:45+00:00", "updated_at": "2026-10-10 14:17:08.436589+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["dasSDL3", "SDL3", "daslang", "dasClangBind", "GaijinEntertainment", "sdl_boost", "Rust SDL3 bindings", "Vulkan"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/dassdl3-idiomatic-sdl3-bindings-for-daslang", "markdown": "https://wpnews.pro/news/dassdl3-idiomatic-sdl3-bindings-for-daslang.md", "text": "https://wpnews.pro/news/dassdl3-idiomatic-sdl3-bindings-for-daslang.txt", "jsonld": "https://wpnews.pro/news/dassdl3-idiomatic-sdl3-bindings-for-daslang.jsonld"}}