feat(plugin,codec): Windows plugin loading and ffmpeg discovery
- oakplugin host: Win32 LoadLibraryExW/GetProcAddress/FreeLibrary backend (LOAD_WITH_ALTERED_SEARCH_PATH so bundle-sibling DLLs resolve), same dl_open/dl_sym/dlclose surface — the POSIX path is untouched; OFX hosts now compile on Windows - proxymanager: PATH split via std::env::split_paths (Windows ';'), ffmpeg.exe name, Windows candidate locations; split logic unit tested
This commit is contained in:
@@ -320,13 +320,11 @@ impl ProxyManager {
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to searching the system PATH.
|
||||
// Fall back to searching the system PATH (split per platform:
|
||||
// `;` on Windows, `:` elsewhere — see [`split_path_env`]).
|
||||
if let Ok(path_env) = std::env::var("PATH") {
|
||||
for dir in path_env.split(':') {
|
||||
if dir.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let candidate = Path::new(dir).join("ffmpeg");
|
||||
for dir in split_path_env(&path_env) {
|
||||
let candidate = Path::new(&dir).join(ffmpeg_exe_name());
|
||||
if is_executable_file(&candidate) {
|
||||
return absolute(&candidate.to_string_lossy());
|
||||
}
|
||||
@@ -338,13 +336,32 @@ impl ProxyManager {
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
let app_path = application_path();
|
||||
if !app_path.is_empty() {
|
||||
candidates.push(format!("{}/ffmpeg", app_path));
|
||||
candidates.push(format!("{}/{}", app_path, ffmpeg_exe_name()));
|
||||
}
|
||||
candidates.push("/opt/homebrew/bin/ffmpeg".to_string());
|
||||
candidates.push("/usr/local/bin/ffmpeg".to_string());
|
||||
candidates.push("/usr/bin/ffmpeg".to_string());
|
||||
candidates.push("/usr/local/bin/ffmpeg".to_string());
|
||||
|
||||
// Windows-specific install locations (GUI-launched apps can also
|
||||
// start with a minimal PATH there).
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
// The official Windows installer defaults to
|
||||
// %LOCALAPPDATA%\Programs\ffmpeg\bin.
|
||||
if let Ok(local_app_data) = std::env::var("LOCALAPPDATA") {
|
||||
candidates.push(format!(
|
||||
"{}/Programs/ffmpeg/bin/{}",
|
||||
local_app_data,
|
||||
ffmpeg_exe_name()
|
||||
));
|
||||
}
|
||||
// Oak's user configuration directory.
|
||||
if let Ok(config_dir) = FileFunctions::new().get_configuration_location() {
|
||||
candidates.push(format!("{}/{}", config_dir, ffmpeg_exe_name()));
|
||||
}
|
||||
}
|
||||
|
||||
for c in candidates {
|
||||
if is_executable_file(Path::new(&c)) {
|
||||
return absolute(&c);
|
||||
@@ -403,12 +420,41 @@ fn application_path() -> String {
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// True when `p` is a regular file with at least one execute bit set.
|
||||
/// Split a PATH-style variable into its non-empty directory entries. The
|
||||
/// separator is platform-specific (`;` on Windows, `:` elsewhere);
|
||||
/// `std::env::split_paths` already applies that rule natively.
|
||||
fn split_path_env(raw: &str) -> Vec<String> {
|
||||
std::env::split_paths(raw)
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.filter(|p| !p.is_empty())
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// The executable file name to look for: `ffmpeg.exe` on Windows,
|
||||
/// `ffmpeg` everywhere else.
|
||||
fn ffmpeg_exe_name() -> &'static str {
|
||||
if cfg!(windows) {
|
||||
"ffmpeg.exe"
|
||||
} else {
|
||||
"ffmpeg"
|
||||
}
|
||||
}
|
||||
|
||||
/// True when `p` is a regular file that can be run as a program. Unix
|
||||
/// checks for at least one execute bit; Windows has no permission bits, so
|
||||
/// any regular file counts.
|
||||
fn is_executable_file(p: &Path) -> bool {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
match std::fs::metadata(p) {
|
||||
Ok(md) if md.is_file() => md.permissions().mode() & 0o111 != 0,
|
||||
_ => false,
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
match std::fs::metadata(p) {
|
||||
Ok(md) if md.is_file() => md.permissions().mode() & 0o111 != 0,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
matches!(std::fs::metadata(p), Ok(md) if md.is_file())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,6 +715,33 @@ mod tests_extra {
|
||||
assert!(std::path::Path::new(&found).exists(), "found: {found}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn split_path_env_uses_platform_separator() {
|
||||
// The separator is platform-dependent, so the expected input is
|
||||
// gated: `:` on Unix, `;` on Windows.
|
||||
#[cfg(unix)]
|
||||
{
|
||||
assert_eq!(split_path_env("a:b:c"), ["a", "b", "c"]);
|
||||
// Empty entries are skipped.
|
||||
assert_eq!(split_path_env("a::b:"), ["a", "b"]);
|
||||
assert_eq!(split_path_env("solo"), ["solo"]);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
assert_eq!(split_path_env("a;b;c"), ["a", "b", "c"]);
|
||||
assert_eq!(split_path_env("a;;b;"), ["a", "b"]);
|
||||
assert_eq!(split_path_env("solo"), ["solo"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ffmpeg_exe_name_is_platform_specific() {
|
||||
#[cfg(unix)]
|
||||
assert_eq!(ffmpeg_exe_name(), "ffmpeg");
|
||||
#[cfg(windows)]
|
||||
assert_eq!(ffmpeg_exe_name(), "ffmpeg.exe");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_proxy_state_empty_and_working() {
|
||||
// Empty filename -> Missing.
|
||||
|
||||
@@ -71,12 +71,17 @@ const RTLD_LOCAL: c_int = 0x4;
|
||||
#[cfg(target_os = "linux")]
|
||||
const RTLD_LOCAL: c_int = 0x0;
|
||||
|
||||
// Windows has no dlopen/dlsym/dlclose: the POSIX FFI below is compiled
|
||||
// out and replaced by LoadLibraryExW/GetProcAddress/FreeLibrary (see the
|
||||
// `win32` module further down), keeping the same function signatures.
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
extern "C" {
|
||||
fn dlopen(filename: *const c_char, flag: c_int) -> *mut c_void;
|
||||
fn dlsym(handle: *mut c_void, symbol: *const c_char) -> *mut c_void;
|
||||
fn dlclose(handle: *mut c_void) -> c_int;
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
/// 动态加载共享库;失败返回 None。
|
||||
fn dl_open(path: &Path) -> Option<*mut c_void> {
|
||||
let c = CString::new(path.to_str()?).ok()?;
|
||||
@@ -88,6 +93,7 @@ fn dl_open(path: &Path) -> Option<*mut c_void> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
/// 查符号;失败返回 None。
|
||||
fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> {
|
||||
let c = CString::new(name).ok()?;
|
||||
@@ -99,6 +105,7 @@ fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
/// 从 `dlsym` 结果取函数指针(libloading 同款转换;调用方保证符号
|
||||
/// 类型正确)。
|
||||
///
|
||||
@@ -109,6 +116,85 @@ unsafe fn dlsym_fn<T>(handle: *mut c_void, name: &str) -> Option<T> {
|
||||
Some(unsafe { std::mem::transmute_copy(&p) })
|
||||
}
|
||||
|
||||
// ---- Windows dynamic loading (kernel32) ----
|
||||
//
|
||||
// LoadLibraryExW / GetProcAddress / FreeLibrary in place of the POSIX
|
||||
// dlopen family. The Windows handle is an HMODULE, which is pointer-sized
|
||||
// and stored in the same `*mut c_void` slot, so the public signatures
|
||||
// (`dl_open` / `dl_sym` / `dlsym_fn` / `dlclose`) are unchanged.
|
||||
|
||||
/// Windows dynamic loading (`LoadLibraryExW` / `GetProcAddress` /
|
||||
/// `FreeLibrary`, kernel32). The handle is an `HMODULE` kept as
|
||||
/// `*mut c_void` for signature parity with the POSIX path.
|
||||
#[cfg(target_os = "windows")]
|
||||
mod win32 {
|
||||
use super::*;
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
|
||||
/// `LOAD_WITH_ALTERED_SEARCH_PATH` (winbase.h): resolve the loaded
|
||||
/// module's dependent DLLs from the module's own directory first, so a
|
||||
/// plugin bundle's sibling DLLs are found next to the binary.
|
||||
const LOAD_WITH_ALTERED_SEARCH_PATH: u32 = 0x0000_0008;
|
||||
|
||||
#[link(name = "kernel32")]
|
||||
unsafe extern "C" {
|
||||
fn LoadLibraryExW(
|
||||
lp_file_name: *const u16,
|
||||
h_file: *mut c_void,
|
||||
dw_flags: u32,
|
||||
) -> *mut c_void;
|
||||
fn GetProcAddress(h_module: *mut c_void, lp_proc_name: *const c_char) -> *mut c_void;
|
||||
fn FreeLibrary(h_module: *mut c_void) -> c_int;
|
||||
}
|
||||
|
||||
/// Dynamically load a shared library from an arbitrary (possibly
|
||||
/// non-UTF-8) path; `None` on failure. The path is converted to a
|
||||
/// NUL-terminated UTF-16 string for `LoadLibraryExW`.
|
||||
pub(super) fn dl_open(path: &Path) -> Option<*mut c_void> {
|
||||
let wide: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
|
||||
let h = unsafe {
|
||||
LoadLibraryExW(wide.as_ptr(), std::ptr::null_mut(), LOAD_WITH_ALTERED_SEARCH_PATH)
|
||||
};
|
||||
if h.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(h)
|
||||
}
|
||||
}
|
||||
|
||||
/// Look up an exported symbol by name; `None` on failure. The symbol
|
||||
/// name is a narrow (ANSI) string — `OfxGetNumberOfPlugins` /
|
||||
/// `OfxGetPlugin`, both plain ASCII.
|
||||
pub(super) fn dl_sym(handle: *mut c_void, name: &str) -> Option<*mut c_void> {
|
||||
let c = CString::new(name).ok()?;
|
||||
let p = unsafe { GetProcAddress(handle, c.as_ptr()) };
|
||||
if p.is_null() {
|
||||
None
|
||||
} else {
|
||||
Some(p)
|
||||
}
|
||||
}
|
||||
|
||||
/// Function pointer from a `GetProcAddress` result (same transmute as
|
||||
/// the POSIX `dlsym_fn`; the caller guarantees the symbol type).
|
||||
///
|
||||
/// # Safety
|
||||
/// The symbol must actually be that function type.
|
||||
pub(super) unsafe fn dlsym_fn<T>(handle: *mut c_void, name: &str) -> Option<T> {
|
||||
let p = dl_sym(handle, name)?;
|
||||
Some(unsafe { std::mem::transmute_copy(&p) })
|
||||
}
|
||||
|
||||
/// Unload a library (`FreeLibrary`; reference-counted like POSIX
|
||||
/// `dlclose`).
|
||||
pub(super) unsafe fn dlclose(handle: *mut c_void) -> c_int {
|
||||
unsafe { FreeLibrary(handle) }
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
use win32::{dl_open, dl_sym, dlsym_fn, dlclose};
|
||||
|
||||
// ---- OFX 宿主侧结构(ofxCore.h,字段序与 SDK 逐字一致)----
|
||||
|
||||
/// `OfxHost`(ofxCore.h:44)。
|
||||
|
||||
Reference in New Issue
Block a user