commit 87d9f2b8821c7c43370ebf11309fc27c65045db2 Author: Tyler Hallada Date: Sat Jul 4 20:58:42 2020 -0400 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..11c65f4 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,199 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" + +[[package]] +name = "autocfg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" + +[[package]] +name = "base64" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" + +[[package]] +name = "blake2b_simd" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if", + "lazy_static", +] + +[[package]] +name = "dirs" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142995ed02755914747cc6ca76fc7e4583cd18578746716d0508ea6ed558b9ff" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9457b06509d27052635f90d6466700c65095fdf75409b3fbdd903e988b886f49" + +[[package]] +name = "log" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b6052be84e6b71ab17edffc2eeabf5c2c3ae1fdb464aae35ac50c67a44e1f7" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "redox_syscall" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2439c63f3f6139d1b57529d16bc3b8bb855230c8efcc5d3a896c8bea7c3b1e84" + +[[package]] +name = "redox_users" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" +dependencies = [ + "getrandom", + "redox_syscall", + "rust-argon2", +] + +[[package]] +name = "rust-argon2" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" +dependencies = [ + "base64", + "blake2b_simd", + "constant_time_eq", + "crossbeam-utils", +] + +[[package]] +name = "rust-skse-plugin" +version = "0.1.0" +dependencies = [ + "dirs", + "log", + "simple-logging", +] + +[[package]] +name = "simple-logging" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b00d48e85675326bb182a2286ea7c1a0b264333ae10f27a937a72be08628b542" +dependencies = [ + "lazy_static", + "log", + "thread-id", +] + +[[package]] +name = "thread-id" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbf4c9d56b320106cd64fd024dadfa0be7cb4706725fc44a7d7ce952d820c1" +dependencies = [ + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..d64fbfe --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "rust-skse-plugin" +version = "0.1.0" +authors = ["Tyler Hallada "] +edition = "2018" + +[dependencies] +log = "0.4" +simple-logging = "2.0" +dirs = "3.0" + +[lib] +name = "RustSKSEPlugin" +crate-type = ["cdylib"] \ No newline at end of file diff --git a/src/README.md b/src/README.md new file mode 100644 index 0000000..e91d670 --- /dev/null +++ b/src/README.md @@ -0,0 +1,81 @@ +# Rust SKSE Plugin + +This is my mostly failed attempt at creating a fully-Rust [SKSE (Skyrim Script Extender)](https://skse.silverlock.org/) plugin. + +## Build and Install + +1. Have Skyrim Special Edition version 1.5.97 or later installed with SKSE build 2.0.17 installed. +2. Checkout the repo and `cd rust-skse-plugin`. +3. Run `cargo build`. +4. Copy `target/debug/RustSKSEPlugin.dll` to `\Data\SKSE\Plugins\`. +5. Start Skyrim Special Edition by running `skse_loader.exe`. +6. Open `\My Games\Skyrim Special Edition\SKSE\skse64.log`, and you should see this plugin being loaded: + ``` + checking plugin E:\Program Files (x86)\Steam\steamapps\common\Skyrim Special Edition\Data\SKSE\Plugins\\RustSKSEPlugin.dll + plugin E:\Program Files (x86)\Steam\steamapps\common\Skyrim Special Edition\Data\SKSE\Plugins\\RustSKSEPlugin.dll (00000001 My Rust SKSE Plugin 00000001) loaded correctly + ``` +7. Open `\My Games\Skyrim Special Edition\SKSE\RustSKSEPlugin.log`, and your should see some logs from the Rust SKSE plugin: + ``` + [00:00:00.000] (8c4) INFO SKSEPlugin_Query begin + [00:00:00.000] (8c4) INFO SKSEPlugin_Query successful + [00:00:00.000] (8c4) INFO SKSEPlugin_Load begin + [00:00:00.000] (8c4) INFO queryInterfaceFunc: 0x7ffea09c695a + [00:00:00.000] (8c4) INFO queryInterface: 0x7ffea0e4eba8 + [00:00:00.000] (8c4) INFO papyrusInterface: 0x7ffea0e4eba8 + [00:00:00.000] (8c4) INFO papyrusRegister: 0x7ffea09d2011 + [00:00:00.000] (8c4) INFO SKSEPlugin_Load successful + [00:00:01.740] (7838) INFO RegisterFuncs begin + [00:00:01.740] (7838) INFO a_registry: 0x19510f93780 + [00:00:01.740] (7838) INFO registerFunction: 0x7ff7c85384f0 + [00:00:01.740] (7838) INFO nativeFunction: 0x26ebebf108 + [00:00:01.740] (7838) INFO RegisterFuncs successful + ``` + +## Where I got stuck + +I was trying to replicate [Ryan-rsm-McKenzie's Native SKSE64 Papyrus Interface Implementation example](https://gist.github.com/Ryan-rsm-McKenzie/cabb89a80abb09663a1288cafddd21e6) in Rust via it's FFI to C. I was able to successfully register the plugin with SKSE and even acquire a reference to the `PapyrusInterface`. + +However, it seems like the code needs to call a C++ constructor (e.g. `new NativeFunction0("HelloWorld", "MyClass", HelloWorld, a_registry)`) in order to register a new native Papyrus function. Unfortunately, calling a C++ constructor requires FFI with C++, and [Rust does not support FFI with C++](https://stackoverflow.com/a/45540511). + +## Failed attempt at using cpp crate + +The [`cpp`](https://docs.rs/cpp/0.5.4/cpp/) crate may provide a way to interface with the SKSE C++ classes, but I'm too inexperienced with C++ to figure out how to compile skse64 in the `build.rs`. + +I was able to get `CommonLibSSE` to compile, but I could not figure out how to convert the abstract `RE::BSScript::IVirtualMachine` class that the `RegisterFuncs` function recieves into something useable on the Rust side. That work is not included in the source, but to get that to work you will need to first follow the [CommonLibSSE setup](https://github.com/Ryan-rsm-McKenzie/CommonLibSSE/wiki/Getting-Started#building-your-first-plugin) and make sure everything builds in Visual Studio first. + +Then checkout this repo alongside the `CommonLibSSE` folder inside `skse64`. Add the `cpp` and `cpp_build` crates to `Cargo.toml` and create this `build.rs` file at the root of the project: + +```rust +extern crate cpp_build; + +fn main() { + let include_path = r#"\skse64\CommonLibSSE\include"#; + let lib_path = r#"\skse64\x64\Debug"#; + let vcpkg_include_path = r#"\installed\x64-windows-custom\include"#; + cpp_build::Config::new().include(include_path).include(vcpkg_include_path).flag("/std:c++17").build("src/lib.rs"); + println!("cargo:rustc-link-search={}", lib_path); + println!("cargo:rustc-link-lib=CommonLibSSE"); +} +``` + +And, add this to `src/lib.rs`: + +```rust +#[macro_use] +extern crate cpp; + +cpp!{{ + #include "RE/Skyrim.h" + #include "REL/Relocation.h" + #include "SKSE/SKSE.h" +}} +``` + +Now you can replace the `SKSEPapyrusInterface` struct with something like: +```rust +cpp_class!(pub unsafe struct PapyrusInterface as "SKSE::PapyrusInterface"); +``` + +## What I'm probably going to do instead + +Abandon my dream of a pure Rust SKSE plugin and just write a normal C++ one (with [CommonLibSSE](https://github.com/Ryan-rsm-McKenzie/CommonLibSSE)) which will execute functions exported from a Rust dll (with [cbindgen](https://crates.io/crates/cbindgen)) inside a native Papyrus function callback. This requires the user to place the Rust-generated dll file in their Skyrim install directory in addition to placing the C++ generated SKSE plugin dll in the SKSE plugins directory. \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..4cbe045 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,185 @@ +#![allow(non_snake_case)] +extern crate simple_logging; +extern crate log; +extern crate dirs; + +use std::ffi::CString; +use std::os::raw::{c_char, c_void}; +use std::path::Path; +use log::{info, error, LevelFilter}; + +type PluginHandle = u32; +#[repr(C)] +pub struct SKSEInterface { + pub skseVersion: u32, + pub runtimeVersion: u32, + pub editorVersion: u32, + pub isEditor: u32, + pub QueryInterface: fn(KInterface) -> *const c_void, + pub GetPluginHandle: fn() -> PluginHandle, + pub GetReleaseIndex: fn() -> u32, +} + +#[repr(C)] +pub struct PluginInfo { + pub infoVersion: u32, + pub name: *mut c_char, + pub version: u32, +} + +#[repr(C)] +pub struct VMValue; + +#[repr(C)] +pub struct VMState; + +#[repr(C)] +pub struct NativeFunction { + InitParams: fn(*const VMClassRegistry) -> (), + Run: fn(*const VMValue, *const VMClassRegistry, u32, *const VMValue, *const VMState) -> bool, +} + +impl NativeFunction { + fn new() -> NativeFunction { + NativeFunction { + InitParams: |registry| { + info!("NativeFunction InitParams begin"); + info!("registry: {:?}", registry); + info!("NativeFunction InitParams successful"); + }, + Run: |baseValue, registry, stackId, resultValue, state| { + info!("NativeFunction Run begin"); + info!("baseValue: {:?}", baseValue); + info!("registry: {:?}", registry); + info!("stackId: {:?}", stackId); + info!("resultValue: {:?}", resultValue); + info!("state: {:?}", state); + info!("NativeFunction Run successful"); + true + }, + } + } +} + + +#[repr(C)] +pub struct VMClassInfo; + +#[repr(C)] +pub struct VMIdentifier; + +#[repr(C)] +pub struct StringCacheRef; + +#[repr(C)] +pub struct VMClassRegistry { + pub Unk_01: fn(c_void) -> c_void, + pub PrintToDebugLog: fn(*const c_char, u32, u32) -> c_void, + pub Unk_03: fn(c_void) -> c_void, + pub Unk_04: fn(c_void) -> c_void, + pub Unk_05: fn(c_void) -> c_void, + pub Unk_06: fn(c_void) -> c_void, + pub Unk_07: fn(c_void) -> c_void, + pub RegisterForm: fn(u32, *const c_char) -> c_void, + pub Unk_09: fn(c_void) -> c_void, + pub GetFormTypeClass: fn(u32, *const VMClassInfo) -> bool, + pub Unk_0B: fn(c_void) -> c_void, + pub Unk_0C: fn(c_void) -> c_void, + pub Unk_0D: fn(*const StringCacheRef, *const u32) -> bool, + pub Unk_0E: fn(c_void) -> c_void, + pub Unk_0F: fn(c_void) -> c_void, + pub Unk_10: fn(c_void) -> c_void, + pub Unk_11: fn(c_void) -> c_void, + pub Unk_12: fn(c_void) -> c_void, + pub Unk_13: fn(c_void) -> c_void, + pub Unk_14: fn(c_void) -> c_void, + pub Unk_15: fn(*const StringCacheRef, *const VMIdentifier) -> bool, + pub CreateArray: fn(*const VMValue, u32, *const VMValue) -> bool, + pub Unk_17: fn(c_void) -> c_void, + pub RegisterFunction: fn(*const NativeFunction) -> c_void, + // more... +} + +type RegisterFunctions = fn(*const VMClassRegistry) -> bool; + +#[repr(C)] +pub struct SKSEPapyrusInterface { + pub interfaceVersion: u32, + pub Register: fn(*const RegisterFunctions) -> bool, +} + +#[repr(u32)] +pub enum KInterface { + Invalid = 0, + Scaleform, + Papyrus, + Serialization, + Task, + Messaging, + Object, + Max, +} + +const fn make_exe_version(major: u32, minor: u32, build: u32, sub: u32) -> u32 { + (((major) & 0xFF) << 24) | (((minor) & 0xFF) << 16) | (((build) & 0xFFF) << 4) | ((sub) & 0xF) +} + +const RUNTIME_VERSION_1_5_97: u32 = make_exe_version(1, 5, 97, 0); + +unsafe fn RegisterFuncs(a_registry: *const VMClassRegistry) -> bool { + info!("RegisterFuncs begin"); + info!("a_registry: {:?}", a_registry); + let registerFunction = (*a_registry).RegisterFunction; + info!("registerFunction: {:?}", registerFunction); + let nativeFunction: *const NativeFunction = &NativeFunction::new(); + info!("nativeFunction: {:?}", nativeFunction); + // This is as far as I can get. I have no idea how to register a native papyrus function. + // Doesn't work, throws exception "Access violation executing location" + // registerFunction(nativeFunction); + info!("RegisterFuncs successful"); + true +} + +#[no_mangle] +pub unsafe extern "C" fn SKSEPlugin_Query(a_skse: *const SKSEInterface, a_info: *mut PluginInfo) -> bool { + let mut log_dir = dirs::document_dir().expect("could not get Documents directory"); + log_dir.push(Path::new(r#"My Games\Skyrim Special Edition\SKSE\RustSKSEPlugin.log"#)); + simple_logging::log_to_file(log_dir, LevelFilter::Info).unwrap(); + info!("SKSEPlugin_Query begin"); + + (*a_info).infoVersion = 1; + (*a_info).name = CString::new("My Rust SKSE Plugin").expect("could not create CString").into_raw(); + (*a_info).version = 1; + + if (*a_skse).isEditor != 0 { + error!("Loaded in editor, marking as incompatible!"); + return false; + } else if (*a_skse).runtimeVersion != RUNTIME_VERSION_1_5_97 { + error!("Unsupported runtime version {}!", (*a_skse).runtimeVersion); + return false; + } + + info!("SKSEPlugin_Query successful"); + true +} + +#[no_mangle] +pub unsafe extern "C" fn SKSEPlugin_Load(a_skse: *const SKSEInterface) -> bool { + info!("SKSEPlugin_Load begin"); + + let queryInterfaceFunc = (*a_skse).QueryInterface; + info!("queryInterfaceFunc: {:?}", queryInterfaceFunc); + let queryInterface = queryInterfaceFunc(KInterface::Papyrus); + info!("queryInterface: {:?}", queryInterface); + let papyrusInterface: *const SKSEPapyrusInterface = queryInterface as *const SKSEPapyrusInterface; + info!("papyrusInterface: {:?}", papyrusInterface); + let papyrusRegister = (*papyrusInterface).Register; + info!("papyrusRegister: {:?}", papyrusRegister); + if !papyrusRegister(RegisterFuncs as *const RegisterFunctions) { + error!("RegisterFuncs returned false!"); + return false; + } + + info!("SKSEPlugin_Load successful"); + true +} \ No newline at end of file