sokol

    Native MoonBit bindings for the Sokol cross-platform C libraries

    sokol
    graphics
    audio
    native
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    24 minutes ago
    Downloads
    2

    No dependencies

    #Sokol for MoonBit

    Native MoonBit bindings for all eight core Sokol headers: app, gfx, audio, fetch, time, args, log, and glue. The binding covers 261 upstream functions, 172 structures, and 42 enum types at the pinned revision 93aa187b5d387cc24b7290b3ca3f13f61e139e7c.

    Sokol's implementation is vendored under native/. MoonBit declarations and C bridges are maintained directly in this repository. Moon executes build.mbtx to supply platform compiler and linker flags; builds do not fetch source files.

    #Build and run

    Install a current MoonBit toolchain and a native C compiler. This repository was validated with Moon 0.1.20261104 and moonc v0.10.20.

    PlatformDefault graphics backendNative dependencies
    macOSMetalXcode command-line tools; system AppKit, QuartzCore, Metal, AudioToolbox
    LinuxOpenGLC compiler; X11, Xi, Xcursor, GL and ALSA development packages
    WindowsD3D11Clang and Windows SDK

    On Debian/Ubuntu, install libx11-dev libxi-dev libxcursor-dev libgl1-mesa-dev libasound2-dev. The implemented build targets are desktop native. Browser Wasm, Android and iOS packaging are not provided by this repository. Platform-specific upstream entry points remain available for native integrations and retain Sokol's own platform restrictions.

    moon run cmd/main # colored triangle; Escape closes the window moon run cmd/main -- --smoke # render ten frames, then quit moon run examples/offscreen # macOS Metal rendering with GPU pixel readback moon check --target all --warn-list +73 moon test

    Run the full graphics/audio lifecycle tests without a window or sound device:

    SOKOL_HEADLESS=1 moon test --target native SOKOL_HEADLESS=1 moon test --target native --release

    SOKOL_HEADLESS=1 selects Sokol's real dummy graphics/audio implementations. It does not emulate a window system. The ordinary moon test run tests portable binding behavior and runs GPU/audio lifecycle tests when built with that flag. On PowerShell, set $env:SOKOL_HEADLESS = "1" before running the test command.

    #Packages and API mapping

    Import only the packages used by your application:

    ///|
    import {
    "Milky2018/sokol/app",
    "Milky2018/sokol/gfx",
    "Milky2018/sokol/glue",
    }

    PackageUpstreamMain APIs
    appsokol_app.hWindow/input/clipboard/drop events, lifecycle, environment, native handles
    gfxsokol_gfx.hBuffers, images, samplers, shaders, pipelines, views, render/compute passes, uploads/copies, queries/stats, trace hooks/listeners
    audiosokol_audio.hAudio configuration, device queries, interleaved PCM push, native callback interop
    fetchsokol_fetch.hAsync and streaming file loading, owned response snapshots, cancellation, pause/continue, buffer binding
    timesokol_time.hMonotonic ticks, lap timing, refresh-rate rounding and unit conversions
    argssokol_args.hArgument parsing, booleans, defaults and indexed queries
    logsokol_log.hStandard Sokol logging
    gluesokol_glue.henvironment() and swapchain() from the active app
    nativeFFI supportExplicitly unowned native pointers and C-string encoding

    Names follow the C API: sg_make_buffer becomes @gfx.make_buffer, sg_buffer_desc becomes @gfx.BufferDesc, and sfetch_error_t becomes @fetch.FetchError. Reserved method names have a trailing underscore, such as event.type_(), view.readonly_(), and @fetch.continue_().

    Configuration fields have getters and in-place set_* setters, including indexed access for fixed-size arrays. Nested getters return independent copies; call copy() explicitly to duplicate a configuration before editing it. Fetch responses are read-only snapshots. Internal canaries and raw range pointer setters are intentionally hidden. Enum types retain upstream integer values and expose to_int(). Native pointer/callback fields use @native.Handle; they require native interoperability lifetime discipline. Generated pkg.generated.mbti files document the public signatures available on the current platform. The binding includes 80 direct native calls, aggregate/bool/width adapters, and explicit callback/IO bridges.

    Platform-specific entry points are checked at compile time. Win32 and D3D11 functions use #cfg(platform="windows"); native macOS/iOS, X11, Android, EGL, HTML5 and Metal entry points use #cfg(not(platform="windows")). These are Windows versus non-Windows restrictions only: the latter does not establish macOS, mobile or browser support, and neither condition selects a graphics backend. OpenGL, WebGPU and general Sokol functions remain cross-platform. Call sites for a restricted function need the same condition. For example:

    ///|
    #cfg(platform="windows")
    fn native_window() -> @native.Handle {
    @app.win32_get_hwnd()
    }

    The checked-in app and gfx interfaces describe non-Windows exports; on Windows, moon info produces the corresponding Windows interface.

    #Descriptors and data ownership

    Descriptors own their copied strings and ranges. set_* modifies the receiver, releases the replaced field, and returns the receiver for chaining. Getters for nested structs return copies: edit that copy and set it back on the parent. copy() duplicates the descriptor and its owned fields. Fixed-array indices are checked before entering C, and C strings reject embedded NUL. Native pointer fields are unowned and do not duplicate or retain a GPU/OS object.

    GPU and fetch handles are typed ID values; copying a handle does not allocate a C object. from_id() supports native ID interoperability. Descriptor unsafe_native_handle() exposes an unowned struct pointer: retain the descriptor and keep it alive across every native access. The complete per-operation ownership contracts and their upstream source references are in OWNERSHIP.md.

    GPU resources have explicit destroy_* functions. Destroy them before @gfx.shutdown() and from the graphics thread; their handles are not GC finalizers. Descriptor-owned C memory is reclaimed by MoonBit. Creating a GPU resource does not retain the input descriptor or uploaded CPU data.

    ///|
    test "owned buffer descriptor" {
    let desc = @gfx.BufferDesc::new()
    .set_label("vertices")
    .set_data(@gfx.Range::from_floats([0.0F, 1.0F, 2.0F]))
    assert_eq(desc.data().size(), 12UL)
    assert_eq(desc.copy().set_label("other").label(), "other")
    assert_eq(desc.label(), "vertices")
    }

    Range::from_bytes, from_floats, from_uints, and from_ushorts own copies of packed data. Pass a range to buffer/image creation, uniform updates, or the upload/copy descriptors. Actual GPU operations retain Sokol's validation rules and report native failures through configured logging and resource-state queries. Use Logger::default() on the relevant setup descriptor to enable C-side logs.

    #Application lifecycle

    @app.run(desc, init~, frame~, event?, cleanup?) keeps captured MoonBit state alive for the application lifetime and copies event objects before invoking the MoonBit callback. Call it on the main OS thread. Callbacks cannot raise across C. Sokol may terminate the process instead of returning from run; release GPU and audio resources in cleanup, and do not depend on code after run.

    The complete triangle is in cmd/main/main.mbt. Its initialization passes @glue.environment() into @gfx.setup; each frame passes @glue.swapchain() into @gfx.begin_pass. Acquire the swapchain once per frame. The offscreen example shows using gfx with an independently created Metal device, and verifies compute dispatch, unsealed buffer uploads, texture uploads and image copies through GPU readback.

    #Shaders, rendering and compute

    ShaderDesc exposes all shader functions, attributes, uniform blocks, texture/storage views, samplers and backend binding metadata. PipelineDesc exposes vertex layout, depth/stencil/blend/rasterization and compute configuration. ViewDesc supports texture, storage and render-attachment views. All dispatch, transient/unsealed upload, sealing, copy, query and statistics APIs are bound.

    For production cross-backend shaders, use the official sokol-shdc to generate C headers. A small user C stub can return the generated const sg_shader_desc*; ShaderDesc::unsafe_from_native copies its strings and bytecode into a managed snapshot. Pin sokol-shdc to a revision compatible with this repository's Sokol revision. examples/shaders also demonstrates writing Metal, GLSL and HLSL source descriptions directly in MoonBit.

    Trace hooks support captured MoonBit closures through every TraceHooks::on_* method. Install with install_trace_hooks; it returns the previous snapshot so hooks can be restored. Build closure hooks from TraceHooks::new(); mixing closure hooks with raw native callbacks or overwriting their userdata is invalid. CommitListener::on_func provides the equivalent closure bridge for commit listeners. Registered hooks/listeners stay alive until replaced, removed, or shutdown, even when the original MoonBit value is released. on_* produces an independent callback configuration and releases obsolete captures when they are no longer referenced. Managed callback configurations cannot be edited using raw native callback/userdata setters. Registered commit listeners must be removed using the same listener identity, not a separately copied callback context.

    #Fetch and audio

    Fetch callbacks run on the calling MoonBit thread when dowork() is called. Each response owns copies of its path and data and can be retained after the request finishes or fetch shuts down. Send reserves the request's native callback and user_data fields for its closure bridge. Capture application state in the callback instead. An invalid/over-capacity request returns a handle with ID zero and never invokes the callback.

    @fetch.setup(@fetch.Desc::new().set_logger(@fetch.Logger::default()))
    let request = @fetch.Request::new()
    .set_path("assets/texture.bin")
    .set_buffer(@fetch.Range::allocate(1024 * 1024))
    let handle = @fetch.send(request, fn(response) {
    if response.fetched() { consume(response.data().bytes()) }
    if response.failed() { report(response.error_code()) }
    })
    // Call @fetch.dowork() in the application's frame callback.
    // When loading ends, or during application cleanup:
    @fetch.shutdown()

    Use cancel, pause, continue_, bind_buffer, and unbind_buffer for request control. Buffer binding/unbinding is restricted to that same request's response callback. Send and bind copy transfer buffers so IO cannot mutate a retained input descriptor. Unbind transfers the active C allocation to the returned range; response snapshots remain independent of that transferred allocation. Streaming requests require an initial buffer at least as large as chunk_size, matching this pinned upstream's validation. A non-streaming request can instead bind its buffer in the dispatch callback. Do not call shutdown or dowork recursively from a response callback. Use a single MoonBit thread for the fetch context.

    Audio uses audio.setup(Desc) and audio.push(FixedArray[Float]) from the MoonBit thread. Samples are interleaved, their count must be divisible by the channel count, and push returns accepted frames. Native Sokol stream/log/ allocator callbacks can run on foreign threads; use C callbacks there, never enter the MoonBit runtime from a device/worker thread. The push interface avoids that runtime restriction. Shut audio down during application cleanup.

    #Timing, arguments and logging

    time.setup() initializes the tick clock; laptime(Ref[UInt64]) updates the last timestamp. args.setup(arguments, desc?) takes an array including the executable name at index zero and copies parsed values. Its optional descriptor supplies capacity and allocator settings; argc/argv are derived from the argument array. Call args.shutdown() when done. log.write(tag, level, message) routes through the standard C logger; Panic level terminates the process. Logger/allocator callback fields remain available for native integrations through explicit unowned handles.

    #Maintainer validation

    Edit the MoonBit and C sources directly. No binding generator, Clang AST parser or pinned C formatter is required. Related APIs live in matching files, such as gfx/buffer.mbt and gfx/buffer.h. Update the public method, its adjacent private FFI declaration, and the C implementation together. Each package's support.c includes internal.h to assemble its native implementation. The helper scripts use only the Python standard library.

    python3 scripts/check_platform_api.py moon check --target all --warn-list +73 SOKOL_HEADLESS=1 moon test --target native SOKOL_HEADLESS=1 moon test --target native --release moon info && moon fmt

    The platform check covers all 32 restricted entry points in both compiler configurations, plus common APIs and the offscreen example. It uses moon check and structured diagnostics in a temporary source copy; it does not depend on toolchain installation paths or change files in the working tree. These are typechecking tests, not Windows runtime validation on a non-Windows host.

    The included sanitizer runner uses MOONBIT_ALLOCATOR=system and a temporary compiler wrapper to instrument every C compile and link step. It builds in a fresh temporary directory without changing manifests or installed toolchain files. The runner supports macOS and Linux. Apple Clang runs ASan without leak detection; Linux enables both ASan and LSan. Callback-capture finalizer tests also check prompt release on both platforms.

    SOKOL_HEADLESS=1 python3 scripts/run-asan.py python3 scripts/run-asan.py --run cmd/main --run examples/offscreen # macOS GPU/app checks

    The compatibility baseline is moonc v0.10.20: callback function references borrow their arguments, and the C bridges release their temporary references after the call. The lifecycle regressions exercise this convention in Debug and Release.

    To update Sokol: fetch the chosen upstream revision and update native/upstream.json with its revision and file hashes. Review the upstream header changes and update the corresponding MoonBit declarations and C bridges directly, including ownership contracts and tests. Run the validation commands above and inspect the public interface changes from moon info. build.mbtx fingerprints shared native headers in the compiler arguments, so header edits trigger C recompilation without rewriting any binding source. Add new shared headers to its header_fingerprint() list. The bridges normalize C ABI boundaries; they do not rely on MoonBit's internal record layout matching C aggregates.

    The bindings are Apache-2.0 licensed. Vendored Sokol retains its original zlib license and copyright notices in native/SOKOL-LICENSE and each header.

    UPSTREAM_REVISION

    let UPSTREAM_REVISION : String

    Sokol revision bundled by this module. Import app/gfx/audio/fetch/etc. directly.

    Source Files