Add a C++ wrapper crate using CXX

This commit is contained in:
Oliver Hamlet
2025-03-25 22:02:13 +00:00
parent 5d53db48cd
commit 3fe91be7e5
13 changed files with 2047 additions and 0 deletions
Generated
+194
View File
@@ -29,6 +29,12 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "arraydeque"
version = "0.5.1"
@@ -77,12 +83,57 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "cc"
version = "1.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be714c154be609ec7f5dad223a33bf1482fff90472de28f7362806e6d4832b8c"
dependencies = [
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "027bb0d98429ae334a8698531da7077bdf906419543a35a55c2cb1b66437d767"
dependencies = [
"clap_builder",
]
[[package]]
name = "clap_builder"
version = "4.5.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5589e0cba072e0f3d23791efac0fd8627b49c829c196a492e88168e6a669d863"
dependencies = [
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_lex"
version = "0.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
[[package]]
name = "codespan-reporting"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e"
dependencies = [
"termcolor",
"unicode-width",
]
[[package]]
name = "const-random"
version = "0.1.18"
@@ -162,6 +213,65 @@ dependencies = [
"typenum",
]
[[package]]
name = "cxx"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "050906babad73f9b32a91cecc3063ff1e2235226dd2367dd839fd6fbc941c68a"
dependencies = [
"cc",
"cxxbridge-cmd",
"cxxbridge-flags",
"cxxbridge-macro",
"foldhash",
"link-cplusplus",
]
[[package]]
name = "cxx-build"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875d58f2ac56025a775b91a424515b5adf1e68205765f2c90e6dd81e269ae004"
dependencies = [
"cc",
"codespan-reporting",
"proc-macro2",
"quote",
"scratch",
"syn",
]
[[package]]
name = "cxxbridge-cmd"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19c3062da294104183e1c34ea9887941d4d8c74f6195ce9fbb430ac4b5290ede"
dependencies = [
"clap",
"codespan-reporting",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "cxxbridge-flags"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4b358173a166833ddef75fe468579f71727c789b8082d4cc77c38d08f656c59"
[[package]]
name = "cxxbridge-macro"
version = "1.0.143"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9531217f3b5f7728244d2b7312bc6660f6b3e4cdbc118f4f1fbce48cb401a0f"
dependencies = [
"proc-macro2",
"quote",
"rustversion",
"syn",
]
[[package]]
name = "dataview"
version = "1.0.1"
@@ -171,6 +281,17 @@ dependencies = [
"derive_pod",
]
[[package]]
name = "delegate"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297806318ef30ad066b15792a8372858020ae3ca2e414ee6c2133b1eb9e9e945"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_pod"
version = "0.1.2"
@@ -283,6 +404,12 @@ version = "0.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99"
[[package]]
name = "foldhash"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
[[package]]
name = "futures-core"
version = "0.3.31"
@@ -454,6 +581,16 @@ dependencies = [
"windows",
]
[[package]]
name = "libloot-cxx"
version = "0.25.3"
dependencies = [
"cxx",
"cxx-build",
"delegate",
"libloot",
]
[[package]]
name = "libredox"
version = "0.1.3"
@@ -464,6 +601,15 @@ dependencies = [
"libc",
]
[[package]]
name = "link-cplusplus"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a6f6da007f968f9def0d65a05b187e2960183de70c160204ecfccf0ee330212"
dependencies = [
"cc",
]
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -841,6 +987,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "rustversion"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2"
[[package]]
name = "saphyr"
version = "0.0.3"
@@ -863,6 +1015,12 @@ dependencies = [
"hashlink",
]
[[package]]
name = "scratch"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f6280af86e5f559536da57a45ebc84948833b3bee313a7dd25232e09c878a52"
[[package]]
name = "semver"
version = "1.0.26"
@@ -880,6 +1038,12 @@ dependencies = [
"digest",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "slab"
version = "0.4.9"
@@ -889,6 +1053,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.98"
@@ -914,6 +1084,15 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "termcolor"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
dependencies = [
"winapi-util",
]
[[package]]
name = "thiserror"
version = "1.0.69"
@@ -1010,6 +1189,12 @@ version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
[[package]]
name = "unicode-width"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
[[package]]
name = "version_check"
version = "0.9.5"
@@ -1047,6 +1232,15 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
"windows-sys",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
+3
View File
@@ -24,3 +24,6 @@ windows = { version = "0.60.0", features = ["Win32_Storage_FileSystem"] }
rstest = "0.25.0"
rstest_reuse = "0.7.0"
tempfile = "3.17.1"
[workspace]
members = ["cxx"]
+2
View File
@@ -0,0 +1,2 @@
/build
/include
+41
View File
@@ -0,0 +1,41 @@
cmake_minimum_required(VERSION 2.8)
project(ffi_tests CXX)
include_directories("${CMAKE_SOURCE_DIR}/../target/cxxbridge")
set(CMAKE_CXX_STANDARD 17)
include(FetchContent)
include(GoogleTest)
set(BUILD_GMOCK OFF)
set(gtest_force_shared_crt ON)
set(INSTALL_GTEST OFF)
# Google Test doesn't have a scoped equivalent to BUILD_SHARED_LIBS, so set the
# global value and then set it back to the original value once Google Test has
# been configured.
set(BUILD_SHARED_LIBS_INITIAL ${BUILD_SHARED_LIBS})
set(BUILD_SHARED_LIBS OFF)
FetchContent_Declare(
GTest
URL "https://github.com/google/googletest/archive/refs/tags/v1.15.2.tar.gz"
URL_HASH "SHA256=7b42b4d6ed48810c5362c265a17faebe90dc2373c885e5216439d37927f02926"
FIND_PACKAGE_ARGS)
FetchContent_MakeAvailable(GTest)
if (CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
set (SYSTEM_LIBS pthread dl)
endif ()
if (MSVC)
set (SYSTEM_LIBS ntdll ws2_32 Userenv bcrypt)
endif ()
set (FFI_LIBRARY "${CMAKE_SOURCE_DIR}/../target/debug/libloot_cxx.lib")
add_executable(ffi_cpp_tests "${CMAKE_SOURCE_DIR}/tests/ffi.cpp")
target_link_libraries(ffi_cpp_tests ${FFI_LIBRARY} ${SYSTEM_LIBS} GTest::gtest_main)
enable_testing()
gtest_discover_tests(ffi_cpp_tests DISCOVERY_TIMEOUT 10)
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "libloot-cxx"
version = "0.25.3"
edition = "2024"
license = "GPL-3.0"
[dependencies]
cxx = { version = "1.0", features = ["c++17"] }
delegate = "0.13.2"
libloot = { path = ".." }
[build-dependencies]
cxx-build = "1.0"
[lib]
crate-type = ["staticlib"]
+57
View File
@@ -0,0 +1,57 @@
# libloot-rs C++ wrapper
This is an **imcomplete** and **experimental** wrapper around the Rust reimplementation of libloot that provides a C++ interface.
## Current status
- [x] Expose all API types, aside from `LogLevel` and error types
- [x] Expose all getters and setters on metadata types
- [x] Expose all other methods on `PluginMetadata`
- [x] Expose creator functions (`new_*`) for all metadata types
- [x] Expose `Group::DEFAULT_NAME` (using an accessor function)
- [x] Expose `MessageContent::DEFAULT_LANGUAGE` (using an accessor function)
- [x] Expose `select_message_content()`
- [x] Expose `is_compatible()`
- [x] Expose `libloot_revision()`
- [x] Expose `libloot_version()`
- [x] Expose `LIBLOOT_VERSION_*` constants (as unmangled extern C statics)
- [ ] Expose `LogLevel`
- [ ] Expose `set_logging_callback()`
- [ ] Write tests to cover API usage patterns
- [ ] Write a C++ layer that provides the same API as the C++ implementation of libloot
## Building
The wrapper is currently built as a static library using Cargo and cbindgen.
```
cargo build --release
cbindgen -o include/libloot.h
```
cbindgen is needed to create a header that contains the C FFI exports. It needs to be v0.28.0 or higher to support the Rust 2024 edition's unsafe attributes. The header needs to be included into C++ code separately from the CXX-generated header. While CXX does support including headers, it produces the wrong include path in the C++ it generates due to this crate being a member of a workspace, so the C++ side expects the header in a subdirectory that doesn't exist.
There are also a small number of C++ tests that can be built and run using CMake:
```
cmake -B build .
cmake --build build --config RelWithDebInfo
ctest --test-dir build --output-on-failure -V
```
To package the build:
```
cpack --config build/CPackConfig.cmake
```
This repository (and so the created package) doesn't currently include any of libloot's documentation, besides the API documentation included in the Rust source code.
## Usage notes
- `LIBLOOT_VERSION_MAJOR`, `LIBLOOT_VERSION_MINOR` and `LIBLOOT_VERSION_PATCH` are exposed as `extern "C"` `static unsigned int` globals.
- CXX doesn't provide integration between Rust's `Option<_>` and C++'s `std::optional<T>`, so sentinel values are used to communicate the absence of a value in the wrapper's API:
- For `Option<&str>`, `None` is represented using an empty string, which is how it's already done for many metadata object fields in the C++ implementation of libloot.
- For `Option<f32>`, `None` is represented using `NaN`, since the C++ implementation of libloot's public API already says that `NaN` values get converted to `None`.
- Plugin CRCs (which are `Option<u32>` in Rust) are represented as `int64_t`, with `-1` representing `None`, since all `uint32_t` values are possibly valid CRCs.
- All Rust errors are converted to `::rust::Error` exceptions that have a `what()` string that is the concatenation of the error's display string and all its recursive source error display strings.
+7
View File
@@ -0,0 +1,7 @@
fn main() {
cxx_build::bridge("src/lib.rs")
.std("c++17")
.compile("libloot-cxx");
println!("cargo:rerun-if-changed=src/lib.rs");
}
+1
View File
@@ -0,0 +1 @@
namespace = "loot"
+281
View File
@@ -0,0 +1,281 @@
use std::{
path::Path,
sync::{Arc, RwLock},
};
use delegate::delegate;
use libloot::error::DatabaseLockPoisonError;
use crate::{
UnsupportedEnumValueError, VerboseError,
ffi::EdgeType,
metadata::{Group, Message, OptionalPluginMetadata, PluginMetadata, to_vec_of_unwrapped},
};
#[derive(Debug)]
#[repr(transparent)]
pub struct Database(Arc<RwLock<libloot::Database>>);
impl Database {
pub fn new(db: Arc<RwLock<libloot::Database>>) -> Self {
Self(db)
}
pub fn load_masterlist(&self, path: &str) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.load_masterlist(Path::new(path))
.map_err(Into::into)
}
pub fn load_masterlist_with_prelude(
&self,
masterlist_path: &str,
prelude_path: &str,
) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.load_masterlist_with_prelude(Path::new(masterlist_path), Path::new(prelude_path))
.map_err(Into::into)
}
pub fn load_userlist(&self, path: &str) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.load_userlist(Path::new(path))
.map_err(Into::into)
}
pub fn write_user_metadata(
&self,
output_path: &str,
overwrite: bool,
) -> Result<(), VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.write_user_metadata(Path::new(output_path), overwrite)
.map_err(Into::into)
}
pub fn write_minimal_list(
&self,
output_path: &str,
overwrite: bool,
) -> Result<(), VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.write_minimal_list(Path::new(output_path), overwrite)
.map_err(Into::into)
}
pub fn known_bash_tags(&self) -> Result<Vec<String>, VerboseError> {
Ok(self
.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.known_bash_tags())
}
pub fn general_messages(
&self,
evaluate_conditions: bool,
) -> Result<Vec<Message>, VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.general_messages(evaluate_conditions)
.map(|v| v.into_iter().map(Into::into).collect())
.map_err(Into::into)
}
pub fn groups(&self, include_user_metadata: bool) -> Result<Vec<Group>, VerboseError> {
Ok(self
.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.groups(include_user_metadata)
.into_iter()
.map(Into::into)
.collect())
}
// I tried returning a GroupRef<'_> here, but it borrows from the DB read guard, which the borrow checker sees as an owned value within this scope, so won't allow me to return a reference to it. This is the only place that uses a group reference, so it's probably not worth figuring out a workaround, and cloning the groups is fine.
pub fn user_groups(&self) -> Result<Vec<Group>, VerboseError> {
Ok(self
.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.user_groups()
.iter()
.cloned()
.map(Into::into)
.collect())
}
// This is ugly, but Group can't be held as a value in C++ and CXX doesn't support Vec<Box<Group>> as a parameter, so this can't take ownership of the input groups.
pub fn set_user_groups(&self, groups: &[Box<Group>]) -> Result<(), VerboseError> {
let groups = to_vec_of_unwrapped(groups);
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.set_user_groups(groups);
Ok(())
}
pub fn groups_path(
&self,
from_group_name: &str,
to_group_name: &str,
) -> Result<Vec<Vertex>, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.groups_path(from_group_name, to_group_name)
.map(|v| v.into_iter().map(Into::into).collect())
.map_err(Into::into)
}
pub fn plugin_metadata(
&self,
plugin_name: &str,
include_user_metadata: bool,
evaluate_conditions: bool,
) -> Result<Box<OptionalPluginMetadata>, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.plugin_metadata(plugin_name, include_user_metadata, evaluate_conditions)
.map(|p| Box::new(p.map(Into::into).into()))
.map_err(Into::into)
}
pub fn plugin_user_metadata(
&self,
plugin_name: &str,
evaluate_conditions: bool,
) -> Result<Box<OptionalPluginMetadata>, VerboseError> {
self.0
.read()
.map_err(|_| DatabaseLockPoisonError)?
.plugin_user_metadata(plugin_name, evaluate_conditions)
.map(|p| Box::new(p.map(Into::into).into()))
.map_err(Into::into)
}
pub fn set_plugin_user_metadata(
&mut self,
plugin_metadata: Box<PluginMetadata>,
) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.set_plugin_user_metadata(plugin_metadata.into());
Ok(())
}
pub fn discard_plugin_user_metadata(&self, plugin: &str) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.discard_plugin_user_metadata(plugin);
Ok(())
}
pub fn discard_all_user_metadata(&self) -> Result<(), VerboseError> {
self.0
.write()
.map_err(|_| DatabaseLockPoisonError)?
.discard_all_user_metadata();
Ok(())
}
}
#[derive(Clone, Debug)]
#[repr(transparent)]
pub struct Vertex(libloot::Vertex);
pub fn new_vertex(name: String) -> Box<Vertex> {
Box::new(Vertex(libloot::Vertex::new(name)))
}
impl Vertex {
// A value of 255 is used to indicate that there is no out edge.
pub fn out_edge_type(&self) -> Result<u8, VerboseError> {
match self.0.out_edge_type() {
Some(e) => Ok(EdgeType::try_from(e)?.repr),
None => Ok(u8::MAX),
}
}
pub fn set_out_edge_type(&mut self, out_edge_type: EdgeType) -> Result<(), VerboseError> {
self.0.set_out_edge_type(out_edge_type.try_into()?);
Ok(())
}
pub fn boxed_clone(&self) -> Box<Self> {
Box::new(Self(self.0.clone()))
}
delegate! {
to self.0 {
pub fn name(&self) -> &str;
}
}
}
impl From<libloot::Vertex> for Vertex {
fn from(value: libloot::Vertex) -> Self {
Self(value)
}
}
impl TryFrom<libloot::EdgeType> for EdgeType {
type Error = UnsupportedEnumValueError;
fn try_from(value: libloot::EdgeType) -> Result<Self, Self::Error> {
match value {
libloot::EdgeType::Hardcoded => Ok(EdgeType::hardcoded),
libloot::EdgeType::MasterFlag => Ok(EdgeType::masterFlag),
libloot::EdgeType::Master => Ok(EdgeType::master),
libloot::EdgeType::MasterlistRequirement => Ok(EdgeType::masterlistRequirement),
libloot::EdgeType::UserRequirement => Ok(EdgeType::userRequirement),
libloot::EdgeType::MasterlistLoadAfter => Ok(EdgeType::masterlistLoadAfter),
libloot::EdgeType::UserLoadAfter => Ok(EdgeType::userLoadAfter),
libloot::EdgeType::MasterlistGroup => Ok(EdgeType::masterlistGroup),
libloot::EdgeType::UserGroup => Ok(EdgeType::userGroup),
libloot::EdgeType::RecordOverlap => Ok(EdgeType::recordOverlap),
libloot::EdgeType::AssetOverlap => Ok(EdgeType::assetOverlap),
libloot::EdgeType::TieBreak => Ok(EdgeType::tieBreak),
libloot::EdgeType::BlueprintMaster => Ok(EdgeType::blueprintMaster),
_ => Err(UnsupportedEnumValueError),
}
}
}
impl TryFrom<EdgeType> for libloot::EdgeType {
type Error = UnsupportedEnumValueError;
fn try_from(value: EdgeType) -> Result<Self, Self::Error> {
match value {
EdgeType::hardcoded => Ok(libloot::EdgeType::Hardcoded),
EdgeType::masterFlag => Ok(libloot::EdgeType::MasterFlag),
EdgeType::master => Ok(libloot::EdgeType::Master),
EdgeType::masterlistRequirement => Ok(libloot::EdgeType::MasterlistRequirement),
EdgeType::userRequirement => Ok(libloot::EdgeType::UserRequirement),
EdgeType::masterlistLoadAfter => Ok(libloot::EdgeType::MasterlistLoadAfter),
EdgeType::userLoadAfter => Ok(libloot::EdgeType::UserLoadAfter),
EdgeType::masterlistGroup => Ok(libloot::EdgeType::MasterlistGroup),
EdgeType::userGroup => Ok(libloot::EdgeType::UserGroup),
EdgeType::recordOverlap => Ok(libloot::EdgeType::RecordOverlap),
EdgeType::assetOverlap => Ok(libloot::EdgeType::AssetOverlap),
EdgeType::tieBreak => Ok(libloot::EdgeType::TieBreak),
EdgeType::blueprintMaster => Ok(libloot::EdgeType::BlueprintMaster),
_ => Err(UnsupportedEnumValueError),
}
}
}
+196
View File
@@ -0,0 +1,196 @@
use std::path::Path;
use delegate::delegate;
use crate::{
PluginRef, UnsupportedEnumValueError, VerboseError, database::Database, ffi::GameType,
plugin::OptionalPluginRef,
};
impl TryFrom<libloot::GameType> for GameType {
type Error = UnsupportedEnumValueError;
fn try_from(value: libloot::GameType) -> Result<Self, Self::Error> {
match value {
libloot::GameType::TES4 => Ok(GameType::tes4),
libloot::GameType::TES5 => Ok(GameType::tes5),
libloot::GameType::FO3 => Ok(GameType::fo3),
libloot::GameType::FONV => Ok(GameType::fonv),
libloot::GameType::FO4 => Ok(GameType::fo4),
libloot::GameType::TES5SE => Ok(GameType::tes5se),
libloot::GameType::FO4VR => Ok(GameType::fo4vr),
libloot::GameType::TES5VR => Ok(GameType::tes5vr),
libloot::GameType::TES3 => Ok(GameType::tes3),
libloot::GameType::Starfield => Ok(GameType::starfield),
libloot::GameType::OpenMW => Ok(GameType::openmw),
_ => Err(UnsupportedEnumValueError),
}
}
}
impl TryFrom<GameType> for libloot::GameType {
type Error = UnsupportedEnumValueError;
fn try_from(value: GameType) -> Result<Self, Self::Error> {
match value {
GameType::tes4 => Ok(libloot::GameType::TES4),
GameType::tes5 => Ok(libloot::GameType::TES5),
GameType::fo3 => Ok(libloot::GameType::FO3),
GameType::fonv => Ok(libloot::GameType::FONV),
GameType::fo4 => Ok(libloot::GameType::FO4),
GameType::tes5se => Ok(libloot::GameType::TES5SE),
GameType::fo4vr => Ok(libloot::GameType::FO4VR),
GameType::tes5vr => Ok(libloot::GameType::TES5VR),
GameType::tes3 => Ok(libloot::GameType::TES3),
GameType::starfield => Ok(libloot::GameType::Starfield),
GameType::openmw => Ok(libloot::GameType::OpenMW),
_ => Err(UnsupportedEnumValueError),
}
}
}
impl From<Game> for libloot::Game {
fn from(value: Game) -> Self {
value.0
}
}
impl From<libloot::Game> for Game {
fn from(value: libloot::Game) -> Self {
Game(value)
}
}
#[derive(Debug)]
pub struct NotValidUtf8;
impl std::fmt::Display for NotValidUtf8 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Value was not valid UTF-8")
}
}
impl std::error::Error for NotValidUtf8 {}
#[derive(Debug)]
#[repr(transparent)]
pub struct Game(libloot::Game);
// CXX doesn't support &Path so use &str instead.
pub fn new_game(game_type: GameType, game_path: &str) -> Result<Box<Game>, VerboseError> {
libloot::Game::new(game_type.try_into()?, Path::new(game_path))
.map(|g| Box::new(g.into()))
.map_err(Into::into)
}
pub fn new_game_with_local_path(
game_type: GameType,
game_path: &str,
game_local_path: &str,
) -> Result<Box<Game>, VerboseError> {
libloot::Game::with_local_path(
game_type.try_into()?,
Path::new(game_path),
Path::new(game_local_path),
)
.map(|g| Box::new(g.into()))
.map_err(Into::into)
}
fn path_to_string(path: &Path) -> Result<String, VerboseError> {
path.to_str()
.map(|s| s.to_owned())
.ok_or(NotValidUtf8)
.map_err(Into::into)
}
fn strings_to_paths<'a>(paths: &'a [&'a str]) -> Vec<&'a Path> {
paths.iter().map(Path::new).collect()
}
impl Game {
pub fn game_type(&self) -> Result<GameType, VerboseError> {
self.0.game_type().try_into().map_err(Into::into)
}
pub fn additional_data_paths(&self) -> Result<Vec<String>, VerboseError> {
self.0
.additional_data_paths()
.iter()
.map(|p| path_to_string(p))
.collect()
}
pub fn set_additional_data_paths(
&mut self,
additional_data_paths: &[&str],
) -> Result<(), VerboseError> {
self.0
.set_additional_data_paths(&strings_to_paths(additional_data_paths))
.map_err(Into::into)
}
pub fn database(&self) -> Box<Database> {
Box::new(Database::new(self.0.database()))
}
pub fn is_valid_plugin(&self, plugin_path: &str) -> bool {
self.0.is_valid_plugin(Path::new(plugin_path))
}
pub fn load_plugins(&mut self, plugin_paths: &[&str]) -> Result<(), VerboseError> {
self.0
.load_plugins(&strings_to_paths(plugin_paths))
.map_err(Into::into)
}
pub fn load_plugin_headers(&mut self, plugin_paths: &[&str]) -> Result<(), VerboseError> {
self.0
.load_plugin_headers(&strings_to_paths(plugin_paths))
.map_err(Into::into)
}
pub fn plugin(&self, plugin_name: &str) -> Box<OptionalPluginRef> {
Box::new(self.0.plugin(plugin_name).into())
}
pub fn loaded_plugins(&self) -> Vec<PluginRef<'_>> {
self.0
.loaded_plugins()
.into_iter()
.map(Into::into)
.collect()
}
pub fn sort_plugins(&self, plugin_names: &[&str]) -> Result<Vec<String>, VerboseError> {
self.0.sort_plugins(plugin_names).map_err(Into::into)
}
pub fn load_current_load_order_state(&mut self) -> Result<(), VerboseError> {
self.0.load_current_load_order_state().map_err(Into::into)
}
pub fn is_load_order_ambiguous(&self) -> Result<bool, VerboseError> {
self.0.is_load_order_ambiguous().map_err(Into::into)
}
pub fn active_plugins_file_path(&self) -> Result<String, VerboseError> {
path_to_string(self.0.active_plugins_file_path())
}
pub fn load_order(&self) -> Vec<String> {
self.0.load_order().iter().map(|s| s.to_string()).collect()
}
pub fn set_load_order(&mut self, load_order: &[&str]) -> Result<(), VerboseError> {
self.0.set_load_order(load_order).map_err(Into::into)
}
delegate! {
to self.0 {
pub fn clear_loaded_plugins(&mut self);
pub fn is_plugin_active(&self, plugin_name: &str) -> bool;
}
}
}
+569
View File
File diff suppressed because it is too large Load Diff
+592
View File
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
use delegate::delegate;
use crate::{EmptyOptionalError, OptionalRef, VerboseError};
#[derive(Debug)]
#[repr(transparent)]
pub struct PluginRef<'a>(&'a libloot::Plugin);
impl<'a> PluginRef<'a> {
pub fn new(plugin: &'a libloot::Plugin) -> Self {
Self(plugin)
}
pub fn header_version(&self) -> f32 {
self.0.header_version().unwrap_or(f32::NAN)
}
pub fn version(&self) -> &str {
self.0.version().unwrap_or("")
}
pub fn masters(&self) -> Result<Vec<String>, VerboseError> {
self.0.masters().map_err(Into::into)
}
pub fn crc(&self) -> i64 {
self.0.crc().map(Into::into).unwrap_or(-1)
}
pub fn is_valid_as_light_plugin(&self) -> Result<bool, VerboseError> {
self.0.is_valid_as_light_plugin().map_err(Into::into)
}
pub fn is_valid_as_medium_plugin(&self) -> Result<bool, VerboseError> {
self.0.is_valid_as_medium_plugin().map_err(Into::into)
}
pub fn is_valid_as_update_plugin(&self) -> Result<bool, VerboseError> {
self.0.is_valid_as_update_plugin().map_err(Into::into)
}
pub fn do_records_overlap(&self, plugin: &Self) -> Result<bool, VerboseError> {
self.0.do_records_overlap(plugin.0).map_err(Into::into)
}
delegate! {
to self.0 {
pub fn name(&self) -> &str;
pub fn bash_tags(&self) -> &[String];
pub fn is_master(&self) -> bool;
pub fn is_light_plugin(&self) -> bool;
pub fn is_medium_plugin(&self) -> bool;
pub fn is_update_plugin(&self) -> bool;
pub fn is_blueprint_plugin(&self) -> bool;
pub fn is_empty(&self) -> bool;
pub fn loads_archive(&self) -> bool;
}
}
}
impl<'a> From<&'a libloot::Plugin> for PluginRef<'a> {
fn from(value: &'a libloot::Plugin) -> Self {
PluginRef(value)
}
}
pub type OptionalPluginRef = OptionalRef<libloot::Plugin>;
impl OptionalRef<libloot::Plugin> {
/// # Safety
///
/// This is safe as long as the pointer in the OptionalRef is still valid.
pub unsafe fn as_ref(&self) -> Result<Box<PluginRef<'_>>, EmptyOptionalError> {
if self.0.is_null() {
Err(EmptyOptionalError)
} else {
unsafe { Ok(Box::new(PluginRef::new(&*self.0))) }
}
}
}