Merge branch 'build/windows-rawdylib' into 'master'

libvlc-sys: import libvlc via raw-dylib on Windows

See merge request videolan/vlc-rs!19
merge-requests/19/merge
Alaric Senat 2026-09-01 10:34:30 +02:00
commit dd0b720c75
9 changed files with 895 additions and 2499 deletions

2
.gitattributes vendored 100644
View File

@ -0,0 +1,2 @@
# Collapse generated bindings on gitlabs MRs
libvlc-sys/bindings.rs gitlab-generated

43
Cargo.lock generated
View File

@ -2,22 +2,6 @@
# It is not intended for manual editing.
version = 3
[[package]]
name = "cc"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890"
[[package]]
name = "libc"
version = "0.2.189"
@ -30,7 +14,6 @@ version = "0.2.0"
dependencies = [
"libc",
"pkg-config",
"vswhom",
]
[[package]]
@ -39,12 +22,6 @@ version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548"
[[package]]
name = "shlex"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "vlc-rs"
version = "0.3.0"
@ -52,23 +29,3 @@ dependencies = [
"libc",
"libvlc-sys",
]
[[package]]
name = "vswhom"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b"
dependencies = [
"libc",
"vswhom-sys",
]
[[package]]
name = "vswhom-sys"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150"
dependencies = [
"cc",
"libc",
]

View File

@ -54,22 +54,11 @@ Other examples are in the examples directory.
### Windows
To build `vlc-rs`, you must either build VLC from source or grab one of the pre-built packages from [videolan.org](https://www.videolan.org/vlc/download-windows.html).
vlc-rs uses libvlc's SDK and it is a required dependency that must be available at runtime.
For that, you must either build VLC from source or grab one of the pre-built packages from [videolan.org](https://www.videolan.org/vlc/download-windows.html).
If you're building for `x86_64`, then you should grab the download labelled "Installer for 64bit version".
That installer is actually a self-extracting ZIP archive, so we can extract the contents without installing VLC itself.
If you're building for `x86`, then you should either download labelled "7zip package" or the one labelled "Zip package".
Once you've downloaded your chosen package, you should extract it some place such that its path contains no spaces.
To point `vlc-rs` at your VLC package, you should set an appropriate environment variable:
- `VLC_LIB_DIR`: Directory of the VLC pacakge, any architecture
- `VLC_LIB_DIR_X86` : Directory of the VLC pacakge, `x86`-only
- `VLC_LIB_DIR_X86_64` : Directory of the VLC pacakge, `x86_64`-only
You should also add the package to your `PATH` variable if you intend to run the program.
For distribution of an executable program, you should probably copy over the neccessary DLLs, as well as the `plugins` directory.
Once you've downloaded your chosen package, you should extract it some place such that its path contains no spaces, and add that directory to your `PATH` so `libvlc.dll` is found at startup.
For distribution of an executable program, you should probably copy over the necessary DLLs, as well as the `plugins` directory.
## License

View File

@ -23,6 +23,3 @@ libc = "0.2"
[build-dependencies]
pkg-config = "0.3"
[target.'cfg(target_os = "windows")'.build-dependencies]
vswhom = "0.1.0"

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,7 @@
//! Build script for `libvlc-sys`, locates and links libvlc.
use std::env;
/// Minimum supported libvlc version.
const MIN_LIBVLC_VERSION: &str = "3.0.0";
@ -10,126 +12,21 @@ fn probe_libvlc() -> Result<pkg_config::Library, pkg_config::Error> {
.probe("libvlc")
}
#[cfg(target_os = "windows")]
mod windows {
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
compile_error!("Only x86 and x86_64 are supported at the moment. Adding support for other architectures should be trivial.");
use std::env;
use std::ffi::OsString;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use vswhom::VsFindResult;
pub fn link_vlc() {
let vlc_path = vlc_path();
let out_dir = PathBuf::from(env::var_os("OUT_DIR").unwrap());
let vs = VsFindResult::search().expect("Could not locate Visual Studio");
let vs_exe_path = PathBuf::from(
vs.vs_exe_path
.expect("Could not retrieve executable path for Visual Studio"),
);
generate_lib_from_dll(&out_dir, &vs_exe_path, &vlc_path);
println!("cargo:rustc-link-search=native={}", out_dir.display());
// NOTE: Without this directive, linking fails with:
// ```
// error LNK2019: unresolved external symbol vsnprintf referenced in function _{MangledSymbolName}
// msvcrt.lib(vsnprintf.obj) : error LNK2001: unresolved external symbol vsnprintf
// msvcrt.lib(vsnprintf.obj) : error LNK2001: unresolved external symbol _vsnprintf
// ```
// https://stackoverflow.com/a/34230122
fn main() {
// vsnprintf is inlined by the UCRT headers, so MSVC needs this to resolve
// the symbol src/lib.rs declares. https://stackoverflow.com/a/34230122
if env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default() == "msvc" {
println!("cargo:rustc-link-lib=dylib=legacy_stdio_definitions");
}
fn generate_lib_from_dll(out_dir: &Path, vs_exe_path: &Path, vlc_path: &Path) {
// https://wiki.videolan.org/GenerateLibFromDll/
let vs_dumpbin = vs_exe_path.join("dumpbin.exe");
let vs_lib = vs_exe_path.join("lib.exe");
let vlc_def_path = out_dir.join("libvlc.def");
let vlc_import_lib = out_dir.join("vlc.lib");
let libvlc = vlc_path.join("libvlc.dll");
let exports = Command::new(vs_dumpbin)
.current_dir(out_dir)
.arg("/EXPORTS")
.arg(libvlc.display().to_string().trim_end_matches(r"\"))
.output()
.unwrap();
let exports = String::from_utf8(exports.stdout).unwrap();
let mut vlc_def = String::from("EXPORTS\n");
for line in exports.lines() {
if let Some(line) = line.get(26..) {
if line.starts_with("libvlc_") {
vlc_def.push_str(line);
vlc_def.push_str("\r\n");
}
}
}
fs::write(&vlc_def_path, vlc_def.into_bytes()).unwrap();
// FIXME: Handle paths with spaces in them.
Command::new(vs_lib)
.current_dir(out_dir)
.arg("/NOLOGO")
.args(&[
format!(
r#"/DEF:{}"#,
vlc_def_path.display().to_string().trim_end_matches(r"\")
),
format!(
r#"/OUT:{}"#,
vlc_import_lib.display().to_string().trim_end_matches(r"\")
),
format!(
"/MACHINE:{}",
match target_arch().as_str() {
"x86" => "x86",
"x86_64" => "x64",
_ => unreachable!(),
}
),
])
.status()
.expect("Failed to run lib.exe")
.success()
.then_some(())
.expect("lib.exe failed to generate the vlc import library");
// The bindings import from libvlc.dll directly via `raw-dylib`, so
// Windows needs neither an import library nor a search path,
// only the DLL at runtime.
if env::var("CARGO_CFG_TARGET_OS").unwrap() == "windows" {
return;
}
fn vlc_path() -> PathBuf {
#[allow(unused_assignments)]
let arch_path: Option<OsString> = match target_arch().as_str() {
"x86" => env::var_os("VLC_LIB_DIR_X86"),
"x86_64" => env::var_os("VLC_LIB_DIR_X86_64"),
_ => unreachable!(),
};
arch_path
.or_else(|| env::var_os("VLC_LIB_DIR"))
.map(PathBuf::from)
.expect("VLC_LIB_DIR not set")
}
fn target_arch() -> String {
env::var("CARGO_CFG_TARGET_ARCH").unwrap()
}
}
fn main() {
// On success pkg-config has already emitted the link directives; only the
// failure path needs handling.
if let Err(err) = probe_libvlc() {
#[cfg(target_os = "windows")]
windows::link_vlc();
#[cfg(not(target_os = "windows"))]
panic!("libvlc (>= {}) not found: {:?}", MIN_LIBVLC_VERSION, err);
}
}

View File

@ -8,3 +8,14 @@ pub mod valist;
// The bindings are a committed source file, regenerated out of band with
// `cargo xtask bindgen`.
include!("../bindings.rs");
// `libc` does not expose vsnprintf and libvlc advises to use it to handle logs in the log
// callbacks. Expose it for convenience.
unsafe extern "C" {
pub fn vsnprintf(
s: *mut libc::c_char,
n: usize,
fmt: *const libc::c_char,
ap: VaList,
) -> libc::c_int;
}

View File

@ -161,7 +161,7 @@ unsafe extern "C" fn logging_cb(
let f: &Box<dyn Fn(LogLevel, Log, Cow<str>) + Send + 'static> = ::std::mem::transmute(data);
let mut buf: [c_char; BUF_SIZE] = [0; BUF_SIZE];
sys::vsnprintf(buf.as_mut_ptr(), BUF_SIZE.try_into().unwrap(), fmt, args);
sys::vsnprintf(buf.as_mut_ptr(), BUF_SIZE, fmt, args);
f((level as u32).into(), Log{ptr: ctx}, from_cstr_ref(buf.as_ptr()).unwrap());
}

View File

@ -43,13 +43,22 @@ fn generate_bindings() {
.ctypes_prefix("libc")
// Allowlist every (lib)vlc symbol.
.allowlist_item("(lib|LIB)?(vlc|VLC)_.*")
// Required by the Windows `legacy_stdio_definitions` link workaround
// (see libvlc-sys/build.rs).
.allowlist_function("vsnprintf")
// Avoid leaking unecessary glibc/platform specifics.
.blocklist_type("_IO_.*")
.blocklist_type("_iobuf")
.blocklist_type("FILE")
.blocklist_type("__off.*")
.blocklist_type("__uint64_t")
.raw_line("pub use libc::FILE;")
// Block the whole va_list family and rewrite the parameters bindings to our own `VaList`,
// which is ABI-correct on every target.
.blocklist_type(".*va_list.*")
.raw_line("pub use crate::valist::VaList;");
.raw_line("pub use crate::valist::VaList;")
// These bindings are committed once and compiled everywhere, so the layout assertions would
// only ever encode the machine that ran bindgen.
.layout_tests(false)
// Emit a single extern block rather than one per function.
.merge_extern_blocks(true);
for path in &library.include_paths {
bindings = bindings.clang_arg(format!("-I{}", path.display()));
@ -68,6 +77,13 @@ fn generate_bindings() {
"a va_list spelling we do not know about survived"
);
// On Windows rustc can synthesise the imports straight from the DLL, so no
// import library is needed. bindgen cannot emit the attribute itself.
let generated = generated.replace(
"unsafe extern \"C\" {",
"#[cfg_attr(windows, link(name = \"libvlc\", kind = \"raw-dylib\"))]\nunsafe extern \"C\" {",
);
std::fs::write(&output, generated).expect("couldn't write bindings");
println!("wrote {}", output.display());
}