mpv-bindgen-sys
Fully automated, luxury low-level Haskell bindings to
libmpv, 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 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_asserts
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 for more.
Most of what you do with libmpv goes through options, commands, and
properties, which the mpv manual
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
Set up your project
- Add
mpv-bindgen-sys to build-depends
- Replace the contents of
Main.hs with:
{-# 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.
For a fuller example, see mpv-headless in
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:
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.
- Run the
pacman commands above through stack exec -- pacman ... so
that the packages are installed in stack's MSYS2.
- Add
msys-environment: UCRT64 to your stack.yaml.
- 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:
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:
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.
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 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:
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"
- Check your libmpv:
pkg-config --modversion mpv. Client API >= 2.0
is required
- Make sure you are on a supported architecture. 32-bit targets are not
presently supported (see Platform support).
- Report it at the issue tracker
with the failing lines, your libmpv version, and your platform.
- If you are comfortable doing so, open a PR regenerating the bindings
from the newer libmpv. The
lithon-codegen README
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
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.
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
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.