packages feed

mpv-bindgen-sys-0.0.0.1: README.md

# mpv-bindgen-sys

Fully automated, luxury low-level Haskell bindings to
[libmpv](https://mpv.io), the library behind the mpv media player: the
whole client API, machine-generated from its headers using an
ever-so-slightly hacked version of `hs-bindgen`.

This package gets you libmpv in full — playback driven through options,
commands, properties, and events; the render API for drawing video into
your own OpenGL context or a memory buffer; and custom stream protocols
backed by your own callbacks.

This package aims to support **64-bit Linux, macOS, and Windows**.

This library aims to be:

- **Complete by construction:** Generated from all four libmpv client API
  headers (`client.h`, `render.h`, `render_gl.h`, `stream_cb.h`), so a
  gap is either a code generation bug or a
  [deliberate omission](#what-is-not-bound) rather than a binding waiting
  to be hand-written.
- **Curated:** The `Mpv.Sys.*` layer wraps `hs-bindgen`'s output with
  best-effort Haskell casing, native scalar types, and FFI safety
  decisions and recommendations.
- **First-class mpv docs:** Every binding carries libmpv's header
  documentation, and notes from the code generator's curation layer.
- **ABI-verified:** A generated translation unit of C `_Static_assert`s
  verifies the library's baked layouts against _your_ libmpv at every
  build. Divergence is a compile error naming the declaration, not
  memory corruption. See [ABI verification](#abi-verification) for more.

Most of what you do with libmpv goes through options, commands, and
properties, which the [mpv manual](https://mpv.io/manual/stable/)
documents rather than the headers.

## Quick start

### Install build requirements

Install libmpv development files for client API **>= 2.0** (mpv 0.35 or
newer) where `pkg-config` can find them as `mpv`. The development
headers must be present, not just the shared library.
`pkg-config --modversion mpv` prints the client API version (e.g.
`2.5.0`), not the mpv release.

Common setups:

- **Debian/Ubuntu**: `apt install libmpv-dev`
- **macOS**: `brew install mpv`
- **Arch**: `pacman -S mpv`
- **Fedora**: `dnf install mpv-devel`
- **Windows**: WSL2 with the Linux instructions, or native MSYS2.
  See [Windows set up](#windows-set-up)

### Set up your project

1. Add `mpv-bindgen-sys` to `build-depends`
2. Replace the contents of `Main.hs` with:

```haskell
{-# LANGUAGE GHC2021 #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE OverloadedRecordDot #-}

import Control.Monad (unless, when)
import Foreign.C.ConstPtr (ConstPtr (..))
import Foreign.C.String (peekCString, withCString)
import Foreign.Ptr (nullPtr)
import Foreign.Storable (peek)
import Mpv.Sys.Client qualified as Mpv

main :: IO ()
main = do
  mpv <- Mpv.create
  when (mpv == nullPtr) (fail "mpv_create failed")
  check "vo" =<< withCString "vo" \name -> withCString "null" \value ->
    Mpv.setOptionString mpv (ConstPtr name) (ConstPtr value)
  check "initialize" =<< Mpv.initializeSafe mpv
  check "loadfile" =<< withCString "loadfile video.mkv" \cmd ->
    Mpv.commandString mpv (ConstPtr cmd)
  let loop = do
        event <- Mpv.waitEventSafe mpv (-1)
        eventId <- peek event.event_id
        case eventId of
          Mpv.MPV_EVENT_END_FILE -> pure ()
          Mpv.MPV_EVENT_SHUTDOWN -> pure ()
          _ -> loop
  loop
  Mpv.terminateDestroySafe mpv
 where
  check what rc = unless (rc >= 0) do
    msg <- peekCString . unConstPtr =<< Mpv.errorString rc
    fail (what <> ": " <> msg)
```

This plays `video.mkv` with the video output disabled (audio still
plays) until the file ends. `initialize`, `waitEvent`, and
`terminateDestroy` can block for a long time, so the example uses their
`Safe` flavor; see [Safe and unsafe FFI](#safe-and-unsafe-ffi).

For a fuller example, see `mpv-headless` in
[`lithon-examples`](https://github.com/jtnuttall/lithon/tree/main/lithon-examples).

### Windows set up

There are two ways to set this up that I am aware of. In order of
convenience:

#### WSL2

The Linux instructions apply unchanged.

#### Native (MSYS2)

Install build dependencies:

```sh
pacman -Syyu # repeat/restart terminal if pacman asks you to
pacman -S mingw-w64-ucrt-x86_64-mpv mingw-w64-ucrt-x86_64-pkgconf
```

> [!NOTE]
> The UCRT64 pkgconf is required. MSYS2 `pkg-config` reports POSIX-style
> paths that GHC can't use on Windows.

##### Stack users

> [!IMPORTANT]
> Stack users need some additional setup.
>
> Adjust the library version in `extra-deps` to your desired target.

1. Run the `pacman` commands above through `stack exec -- pacman ...` so
   that the packages are installed in `stack`'s MSYS2.
2. Add `msys-environment: UCRT64` to your `stack.yaml`.
3. Add `mpv-bindgen-sys-0.0.0.1` to `extra-deps` in your `stack.yaml`. Running
   `stack build` should print out a helpful, pasteable entry for
   this purpose.

Your `stack.yaml` should look something like this:

```yaml
snapshot: lts-24.51
packages:
  - .
extra-deps:
  - mpv-bindgen-sys-0.0.0.1 # hash may be here if you copy from stack build
msys-environment: UCRT64 # important: build will not work without this
```

##### Direct cabal build using an MSYS2 Bash session (e.g., Git Bash)

You can point `cabal` at the UCRT64 toolchain and your ghcup GHC:

```sh
export PKG_CONFIG_PATH="/c/msys64/ucrt64/lib/pkgconfig"
export PATH="/c/msys64/ucrt64/bin:$PATH"
cabal build \
  --with-compiler=/c/ghcup/ghc/9.12.2/bin/ghc.exe \
  --extra-lib-dirs=/c/msys64/ucrt64/lib \
  --extra-include-dirs=/c/msys64/ucrt64/include
```

Adjust the GHC path to match your install. Keep `ucrt64/bin` on `PATH`
when running the program, so Windows finds the libmpv DLL.

## Library structure

For most uses, you will `import Mpv.Sys qualified as Mpv`. `Mpv.Sys`
re-exports one module per libmpv header:

| Module             | Header        | Contents                                                         |
| ------------------ | ------------- | ---------------------------------------------------------------- |
| `Mpv.Sys.Client`   | `client.h`    | Core client API: handles, options, commands, properties, events. |
| `Mpv.Sys.Render`   | `render.h`    | Render API: drive video output from your own rendering loop.     |
| `Mpv.Sys.RenderGl` | `render_gl.h` | OpenGL backend parameters for the render API.                    |
| `Mpv.Sys.StreamCb` | `stream_cb.h` | Custom stream protocols via user callbacks.                      |

The raw hs-bindgen output lives underneath as `Mpv.Sys.Bindgen.*`, if
you need to drop down to C types: each family (`Mpv.Sys.Bindgen.Client`
and so on) carries the types and constants, with the foreign imports in
its `.Safe` and `.Unsafe` modules and each function's address in
`.FunPtr`.

### Safe and unsafe FFI

Most functions come in both FFI flavors: `command` is an `unsafe`
foreign import; `commandSafe` is the `safe` one.

General rules for safe vs. unsafe FFI:

- You _must_ use a `safe` call if C will call back into Haskell.
- You _should_ use a `safe` call if the C call could take a while (e.g.,
  waiting on some OS event, or a locking mechanism, etc.).
- You _should_ use an `unsafe` call if the C call is fast; `unsafe`
  calls block the current GHC thread (capability) and the garbage
  collector, but their overhead is very low compared to `safe` calls.

libmpv makes the first rule bite more often than most C libraries. It
runs your callbacks (the wakeup callback, the render update callback,
custom stream callbacks) on its own threads, and sometimes on yours:
`client.h` warns that the wakeup callback can be called from a thread
while an mpv API function is running. **If any callback you hand to mpv
is a Haskell function, use the `Safe` aliases for everything except the
unsafe-only functions below.**

The curated registry settles the rest:

- **Safe only, because a callback fires during the call:**
  `setWakeupCallbackSafe` and `renderContextSetUpdateCallbackSafe`
  invoke the new callback once immediately, and
  `renderContextCreateSafe` calls the `get_proc_address` you pass in
  `mpv_opengl_init_params`. That last one is curated by hand: the
  callback travels inside the `mpv_render_param` array, where the
  generator's callback census cannot see it.
- **Both flavors, but use the Safe one:** `waitEventSafe` (up to its
  timeout; forever when negative), `waitAsyncRequestsSafe`,
  `initializeSafe`, `destroySafe`, and `terminateDestroySafe` can block
  for a long time (an `unsafe` call would stall the garbage collector
  program-wide meanwhile).
- **Unsafe only:** `clientId`, `clientName`, `errorString`, `eventName`,
  `eventToNode`, `free`, `freeNodeContents`, `getTimeNs`, and `getTimeUs`
  cannot block or call back.
- Every other function exports both. Many of them wait for the playback
  core, which `client.h` says "can take an unbounded time", and several
  run the wakeup callback synchronously.

For the safe-only functions, the genuine unsafe import stays reachable
under the family's `Mpv.Sys.Bindgen.*.Unsafe` module, if you know better.

### Conversion to and from C types

`Mpv.Sys` re-exports `Mpv.Sys.Runtime`, the conversion vocabulary you'll
actually reach for: the `CEnum` classes for moving between enum newtypes
(`Mpv_event_id`, `Mpv_format`, …) and their integral representations.

C scalars in parameters and results arrive as their native Haskell
twins (`int` as `Int32`, `double` as `Double`); struct fields and
pointees keep their C types.

## Platform support

**64-bit platforms only.** Linux, macOS (Homebrew `mpv`), and Windows
(MSYS2 UCRT64, including the LLP64 layouts) are all targets. 32-bit
targets are rejected by the ABI assertions — 64-bit layouts are baked
in.

## Common issues

- **`create` returns `nullPtr`** — libmpv refuses to start unless the
  `LC_NUMERIC` locale category is `"C"` (see "Basic environment
  requirements" in `client.h`). If your program or a GUI toolkit calls
  `setlocale(LC_ALL, "")`, reset `LC_NUMERIC` to `"C"` before `create`.
- **`foo` or `fooSafe`?** — every function's haddock states its flavor
  choice, and the rationale, under its **`mpv-bindgen-sys` notes**
  section. If you registered a Haskell callback, read
  [Safe and unsafe FFI](#safe-and-unsafe-ffi) first.
- **Calling mpv from a callback** — don't. The headers forbid calling
  the API from the wakeup and update callbacks, and a stream callback
  that does can deadlock. Have the callback signal a thread (e.g. with
  `tryPutMVar`) that does the work.
- **The render API and OpenGL** — with the OpenGL backend, the
  `renderContext*` calls need your OpenGL context current on the calling
  OS thread (`render.h`). Make those calls from a bound thread (`main`,
  or one started with `forkOS`): an unbound Haskell thread can move
  between OS threads from one call to the next.
- **`free` is ambiguous** — `mpv_free` is bound as `free`, which clashes
  with `Foreign.Marshal.Alloc.free`. Import `Mpv.Sys` qualified.

## What is not bound

- **`mpv_client_api_version`**: it returns `unsigned long`, whose width
  differs between LP64 and LLP64, and hs-bindgen bakes the raw foreign
  imports' FFI types from the generation host. That is the reason
  `sdl3-bindgen-sys` omits SDL's seven `long`-typed functions, too. The
  `MPV_CLIENT_API_VERSION` constant remains, but it holds the version
  these bindings were generated from (2.5), baked at generation time, not
  the version of the libmpv you link. For the running player, read the
  `mpv-version` property.
- **The `MPV_CPLUGIN_DYNAMIC_SYM` symbol table**: with that macro
  defined, the headers turn every `mpv_*` function into a `pfn_mpv_*`
  function pointer for C plugins loaded into the mpv player. These
  bindings are generated without it and bind the functions directly.
- **`MPV_RENDER_PARAM_DRM_OSD_SIZE`**: a `#define` alias of an enum
  constant, which hs-bindgen does not bind. Use
  `MPV_RENDER_PARAM_DRM_DRAW_SURFACE_SIZE`, the value it names.

The complete list, checked at every generation, is the
[skip ledger](https://github.com/jtnuttall/lithon/blob/main/lithon-codegen/data/mpv/unbound.md):
every declaration hs-bindgen skips, why, and what lithon does about it.

Deprecated declarations are bound, not omitted: `MPV_ENABLE_DEPRECATED`
keeps its default, so `MPV_EVENT_IDLE`, `MPV_EVENT_TICK`, and
`mpv_get_wakeup_pipe` are present.

## libmpv versions

libmpv client API **>= 2.0** (mpv 0.35) is required; the surface is
generated from client API 2.5 (mpv 0.41.0).

Two functions are newer than that floor. They still compile and link
against an older libmpv: their wrapper C is gated on
`MPV_CLIENT_API_VERSION`, and below the gate:

- `delProperty` (`mpv_del_property`, client API 2.1 / mpv 0.36) returns
  `MPV_ERROR_UNSUPPORTED` without calling libmpv.
- `getTimeNs` (`mpv_get_time_ns`, client API 2.2 / mpv 0.37) returns
  `mpv_get_time_us` times 1000: the same clock, at microsecond
  resolution.

Below the gate, their raw `Mpv.Sys.Bindgen.Client.FunPtr` addresses are
`nullFunPtr`.

No struct layout or enum value changed between client API 2.0 and 2.5,
so the ABI assertions check the same layouts on every supported libmpv.
`MPV_RENDER_PARAM_AMBIENT_LIGHT`, deprecated in 2.5, is still defined
and still bound.

## Versioning

The 0.0.x series is experimental: pin to the minor
(e.g., `>=0.0.0.1 && <0.0.1`) and expect surface-shaping changes.

## ABI verification

The cabal flag `abi-assertions` (default) turns on ABI verification,
which guards against unexpected layout divergence between libmpv
versions and varying operating systems.

When ABI assertions are on, `cbits/abi_assertions.c` statically checks
every size, alignment, field offset, and enum value the Haskell side
expects against your libmpv headers.

Maintainers can build with `-f abi-assertions-exact` to assert every
`sizeof` exactly, including structs the default checks only as a
prefix (libmpv has none today). Consumers should leave it off.

### What to do when you get "static assertion failed"

1. Check your libmpv: `pkg-config --modversion mpv`. Client API >= 2.0
   is required
2. Make sure you are on a supported architecture. 32-bit targets are not
   presently supported (see [Platform support](#platform-support)).
3. Report it at the [issue tracker](https://github.com/jtnuttall/lithon/issues)
   with the failing lines, your libmpv version, and your platform.
4. If you are comfortable doing so, open a PR regenerating the bindings
   from the newer libmpv. The
   [`lithon-codegen` README](https://github.com/jtnuttall/lithon/tree/main/lithon-codegen)
   describes the pipeline.

Building with `-f-abi-assertions` turns off the check, not the
mismatch: the bindings would then read and write the baked layout
against headers that disagree with it.

## Known documentation issues

- libmpv documents some struct members and enum constants with a comment
  _after_ the declaration (`char *string; /** valid if … */` in
  `mpv_node`, the `///` level names in `mpv_log_level`). In the
  generated haddocks those comments attach to the _following_ member;
  read them against the header.
- The raw layer keeps hs-bindgen's default type names, the C spelling
  capitalized: `Mpv_node`, `Mpv_event_id`, `Mpv_render_param`.
- `enum mpv_render_update_flag` is bound under its typedef name,
  `Mpv_render_context_flag`; `mpv_node`'s anonymous union is
  `Mpv_node_u`; `struct _drmModeAtomicReq` is the opaque
  `C_DrmModeAtomicReq`.
- Each header's opening overview comment lands in its first
  declaration's haddock (`client.h` → `mPV_MAKE_VERSION`, `render.h` →
  `Mpv_render_context`, `render_gl.h` → `Mpv_opengl_init_params`,
  `stream_cb.h` → `Mpv_stream_cb_read_fn`).

## Provenance and licensing

Generated by [hs-bindgen](https://github.com/well-typed/hs-bindgen)
driven by the repository's `lithon-codegen`, from the libmpv client API
2.5 headers (mpv 0.41.0). The generated tree is never hand-edited, but
bugs are mine, not mpv's or hs-bindgen's: report them at the
[issue tracker](https://github.com/jtnuttall/lithon/issues).

- `mpv-bindgen-sys` is BSD-3-Clause (see `LICENSE`).
- The libmpv client API headers, and the header documentation embedded
  in the haddocks, are ISC-licensed (`LICENSE_libmpv`).
- The vendored hs-bindgen and c-expr runtimes are BSD-3-Clause, (c) Well-Typed LLP
  and Anduril Industries (`LICENSE_hs-bindgen-runtime`, `LICENSE_c-expr-runtime`).

### libmpv's license applies to your program

The ISC license covers the headers, not libmpv. libmpv is GPLv2 or later
by default, and LGPLv2.1 or later only when mpv is built with
`-Dgpl=false` (see mpv's
[`Copyright`](https://github.com/mpv-player/mpv/blob/master/Copyright)
file). A program that links libmpv is subject to libmpv's license,
whatever the license of these bindings.

In practice, shipping a Haskell program built on this package means
shipping libmpv with it, statically linked or bundled, so plan for the
GPL's obligations (or use an LGPL build of libmpv, linked dynamically).

This package only declares the `pkg-config` dependency; how libmpv is
linked is up to your build. For a static link, use the flags
`pkg-config --static --libs mpv` reports.