Have you ever asked yourself what the best way to learn a language in depth is?
Yeah don’t do this I’m kind of masochistic.
Anyway in here I will attempts to unfuck the SuperBLT Native Plugin Template and
SuperBLT Native Plugin Library and rewrite them entirely in rust. This was mainly done for my other Project PD2 Heister’s Haptics.
However rust is far from being my best language, as is C++ so this is and will be a struggle.
Initial Victories
The initial idea was to use mlua to handle all the Lua functionality and then just export the required symbols for SuperBLT to be able to interface with it.
However I can’t read properly so I missed an export for the setup function.
The error for that looks like this:
04:16:21 PM FATAL ERROR: (C:\projects\payday2-superblt\src\InitiateState.cpp:320) mods/HeistersHaptics/mod.lua:11: Invalid dlhandle - missing setup_state func!
Afterwards I simply ran dumpbin /exports
on a dll file for another mod that got linked to me by hoppip in the modworkshop discord server.
The output looked like this:
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
8 number of functions
8 number of names
ordinal hint RVA name
1 0 000061C4 MODULE_LICENCE_DECLARATION
2 1 000061C8 MODULE_SOURCE_CODE_LOCATION
3 2 00006698 MODULE_SOURCE_CODE_REVISION
4 3 000044E0 SBLT_API_REVISION
5 4 00002760 SuperBLT_Plugin_Init_State
6 5 00002770 SuperBLT_Plugin_PushLua
7 6 00002780 SuperBLT_Plugin_Setup
8 7 000027E0 SuperBLT_Plugin_Update
Now after adding a couple lines to my lib.rs
file, I managed to make mine look the same (pretty much anyway).
00000000 characteristics
FFFFFFFF time date stamp
0.00 version
1 ordinal base
8 number of functions
8 number of names
ordinal hint RVA name
1 0 0001836C MODULE_LICENCE_DECLARATION = _MODULE_LICENCE_DECLARATION
2 1 000183A4 MODULE_SOURCE_CODE_LOCATION = _MODULE_SOURCE_CODE_LOCATION
3 2 000183B0 MODULE_SOURCE_CODE_REVISION = _MODULE_SOURCE_CODE_REVISION
4 3 000183B8 SBLT_API_REVISION = _SBLT_API_REVISION
5 4 00001520 SuperBLT_Plugin_Init_State = _SuperBLT_Plugin_Init_State
6 5 00001730 SuperBLT_Plugin_PushLua = _SuperBLT_Plugin_PushLua
7 6 00001580 SuperBLT_Plugin_Setup = _SuperBLT_Plugin_Setup
8 7 00001520 SuperBLT_Plugin_Update = _SuperBLT_Plugin_Init_State
Super BLT would accept this so far as well, however when trying to load the game would crash without any output in the console or log file. Clearly I must still be missing something.
Down thedefine rabbit hole
After talking to Siri for a while I realized, that I should probably use the functions I defined in the SuperBLT_Plugin_Setup
method parameters, as is done in the original.
However the original does use something that I didn’t define at all, which is the all_lua_funcs_list
.
This was defined in the library’s fptrs.h file as a vector of a class AutoFuncSetup
.
Recreating this structure was trivial, however the way this vector was filled with data was anything but. (For the sake of my own sanity I will exclude the code here that is not C++ specific and only gets run in a C context.)
native-plugin-library/include/sblt_msw32_impl/fptrs.h:4-6
Now this basically tells me, that everything run inside of an IMPORT_FUNC
gets turned into an externally defined C
function. So far so good.
However a bit later, we run into this:
native-plugin-library/include/sblt_msw32_impl/fptrs.h:27-30
Now this is only ran in a context where INIT_FUNC
is defined, however this is defined in the initi.cpp of the same repository so I don’t quite get the point of having this here separately but this whole file is a clusterfuck anyway.
This IMPORT_FUNC
definition does the same as the first one, however also creates a new instance of AutoFuncSetup
with the name
and the memory address of the name
cast to void**
.
As far as I’m aware, the reference is not used anywhere, so we can probably safely ignore that part.
This IMPORT_FUNC
is also redefined by something called CREATE_NORMAL_CALLABLE_SIGNATURE
in the same file. That looks like this:
native-plugin-library/include/sblt_msw32_impl/fptrs.h:1
Now then let’s get to where this is used. Here’s an example:
native-plugin-library/include/sblt_msw32_impl/fptrs.h:36
This is quite clear, after running through this macro, it will turn into something like:
AutoFuncSetup
’s constructor also pushes this value into the aforementioned vector.
native-plugin-library/include/sblt_msw32_impl/fptrs.h:20-22
However when reading this information from the vector in the SuperBLT_Plugin_Setup
function, only the name is actually passed into the injected function, not the pointer ptr
.
native-plugin-library/src/msw32/init.cpp:14-17
The function get_exposed_function
is injected internally by SuperBLT and, as far as I can see, reads out the name that’s passed to it, checks if it matches any SuperBLT defined functions and if not passes it further down into get_lua_func
.
payday2-superblt/platforms/w32/plugins/plugins-w32.cpp:42-64
get_lua_func
then filters out getting the lua instance and functions to create a new instance.
Then passes it even further down into GetFunctionByName
.
payday2-superblt/platforms/w32/platform.cpp:190-199
GetFunctionByName
does a couple things but I’ll need some setup to explain this.
payday2-superblt/platforms/w32/signatures/signatures.cpp:411-425
First of all it checks if a variable called allSignatures
is truthy.
allSignatures
is defined as a vector of SignatureF
pointers and initialized as NULL
.
payday2-superblt/platforms/w32/signatures/signatures.cpp:308
SignatureF
is defined as follows:
payday2-superblt/platforms/w32/signatures/signatures.h:14-22
The creation of these happens in the constructor of SignatureSearch
, where the struct is constructed and then pushed into the allSignatures
vector.
payday2-superblt/platforms/w32/signatures/signatures.cpp:308-321
And where is this called? Well this might look a little familiar from the Native Plugin Library
payday2-superblt/platforms/w32/signatures/sigdef.h:6-9
Wow. I’m not going to sugarcoat it, I’ve never felt more disdain for a language than I do right now. Who the fuck even made C++ this was such a dumb decision.
Bjarne Stroustrup born 30 December 1950) is a Danish computer scientist, most notable for the invention and development of the C++ programming language.
Well fuck you too Bjarne Stroustrup. You’re making my life miserable.
Anyway moving on…
Wrong assumptions
This basically means that the following signature from the Native Plugin Library:
native-plugin-library/include/sblt_msw32_impl/fptrs.h:40
Would actually look this like this to SuperBLT:
SignatureVR
is just an enum defined in the same file as SignatureF
and looks as follows:
payday2-superblt/platforms/w32/signatures/signatures.h:7-12
Enlightenment
At least that’s what I thought yesterday, however after looking at the code again and talking it over with Siri for a bit, I came to the conclusion that I’m fucking stupid. It’s not even, that I was wrong about how the code works. I went wrong in thinking the CREATE_NORMAL_CALLABLE_SIGNATURE
define worked the same in both SuperBLT
s code and the Native Plugin Library.
Let’s look at the firstdefine from Down the define rabbit hole again:
native-plugin-library/include/sblt_msw32_impl/fptrs.h:4-6
I correctly assumed, that __VA_ARGS__
just took all the arguments from ...
and shoved them into the function call. However what I failed to realize is that, the ...
here is different from what I expected it to be, since CREATE_NORMAL_CALLABLE_SIGNATURE
redefined ...
before passing it:
native-plugin-library/include/sblt_msw32_impl/fptrs.h:1
What this means is, that in CREATE_NORMAL_CALLABLE_SIGNATURE
all of the weird arguments that I struggled figuring out the meaning of in the last chapter, are actually completely ignored.
No Signature, no mask, and no offset.
So these are actually just normal external definitions, that I should be able to translate to rust as follows:
Well this really is kind of trivial. I should be able to just get all of these in and then get the dll to load right?
Downfall
So remember how in the first chapter, when I set the exports of the dll
to match the ones from the Native Plugin Library code. That I then checked against the Borderless Window Plugin
’s dll
’s exports?
Yeah turns out that I missed one very crucial step but I’ll get to that in a second.
First of all I’ll talk about how I got to finding this out.
After all the effort of previous chapters, I now had hand defined every lua
function that’s defined in the Native Plugin Library/Template in rust but still couldn’t get the game to not crash and burn when loading the dll
. However sadly this also meant, that my dll
didn’t actually want to build. See I assume for C++ to build the library it doesn’t actually link the external symbols at compile time?
I’m not sure though, I just know there’s no lua
lib
/dll
provided in there but the dll
still builds.
The linker ran by Cargo however, actually required that I have a lua
dll
present at compile time, so I gave it one.
After comparing between the Borderless Window mod and mine however, I saw that the Borderless Window mod did in fact not have a lua
dll
included. So I know I did something wrong.
However I ignored this for now and simply commented out every use of lua
functions I had in my code. I had to get the dll
to load first, before I could worry about any of the linking issues.
I wasn’t really sure at which point it was failing and a look through the source code didn’t really give me an indication.
Here are my exported functions:
Now people familiar with SuperBLT might already see something wrong here but for the the rest of you, I’m sure this looks as correct as can be. It did to me anyway.
After running down the functions in the SuperBLT repo for a while, I thought to myself that I’ll need some logging.
So of course I did the most reasonable thing and download the source of SuperBLT.
Opened the folder in Visual Studio (since I kinda have to run this on Windows) and added some logging down the path the plugin load should take me.
payday2-superblt/src/plugins/plugins.cpp:125-149
So on and so forth. Just to see where exactly it is, that I’m getting kicked out and crashing the game. Then I compiled the WSOCK32.dll
and replaced the one in my Payday2 folder with my extra logging version.
And wouldn’t you know it, I found the error pretty quickly… or well at least the place where I errored out.
payday2-superblt/src/InitiateState.cpp:712-743
My self inserted PD2HOOK_LOG_LOG
statement post lua_insert
never got reached.
My first thought was that this problem lies with lua_insert()
but then it would fail on the Borderless Window mod as well. So next I checked if count was > 0
but that log printed.
Then I went back into my own dll
’s code.
count
is set by executing my dll
’s PushLuaValue()
function, that I exposed.
So basically this snippet of code:
Now let’s hop to the definition of Plugin_PushLua()
:
Well what could go wrong here? It couldn’t have been the unsafe
block, since I commented it out because of the linking issues I mentioned at the beginning of the chapter, however still no dice.
I am returning 1 here, as is done in the Native Plugin Template so that can’t really be the issue either.
native-plugin-template/src/main.cpp:20-65
I then commented out the Plugin_PushLua()
function in my SuperBLT_Plugin_PushLua()
and then it hit me.
I never actually returned the 1 from Plugin_PushLua
so count didn’t actually get set to anything in SuperBLT
’s load function. I may be stupid. After changing the function signature a bit and actually returning a 1, I now had a dll
that would actually load and not crash the game!
Now it was time to remove everything that I don’t actually use from my code.
I tried using mlua again to run the lua functions, so I don’t have to rely on the linkers and my jankily defined bindings. And guess what it worked.
I could now delete a solid 60 lines of extern "C"
function definitions for all the lua functions
Also for all the comparisons that I pulled in the Lua constants, I could get rid of these too:
I’m still not entirely sure if this block in SuperBLT_Plugin_Setup()
is required or not but I’ll try removing it and seeing how that works out.
Realistically I shouldn’t need it, since all this does is look up the function name and returning a void*
to the functions address from inside SuperBLT. But that void*
isn’t ever used after this, so I assume I could probably safely ignore this.
I’ll stop here for now though and look more into this tomorrow.
I can absolutely safely resume this part of the code without breaking anything, so I’ll do just that but now to something else equally as cancerous.
Sisyphean Task
As it turns out I like to bash my head against the wall to slowly push through and that’s exactly what I did here. Let me explain…
What I’m currently using to address lua is *mut lua_State
a pointer to the lua_State
that I get from SuperBLT
s loader. This works and all but is kind of ugly to use because all the functions that use it are declared unsafe.
However mlua
actually has it’s own Lua
struct, which has much nicer, safe, functions associated with it. And this thing does have a function to convert from a *mut lua_State
…
I think you catch my drift.
Anyway I spent the next 2 days frantically trying to get it to convert the lua_State
injected from SuperBLT
into an mlua
Lua struct. Sadly without any results.
I did however slowly go insane.
First of all running init_from_ptr()
directly doesn’t work at all. It just kind of crashes when trying to load the dll
, of course again without an error message.
I then downloaded something called patch-crate
(which i prefer to it’s alternative
cargo-patch
, although I seem to have the minority opinion in that) to attempt to:
- Debug my way through to see what the actual issue is in conversion
and - To rectify any conversion issue to actually successfully create the Lua struct.
The result of this was, as said, a 2 day long slow descent into insanity where I would:
- add a
println!()
- see where it stops printing
- check what the issue is
- dump half the lua stack
- point it to the right variable instead
- run it again
over and over and over again.
In-between every step I of course had to build the dll
and put it into the Payday 2 mods folder and start Payday 2 to capture some console output.
Well as it turns out, after a lot of manual manipulation of the mlua
and mlua-sys
crates, that the conversion from the lua_State
pointer provided by SuperBLT
into an mlua
Lua struct is not possible, due to the stack already having some kind of data on it before it’s passed down to me.
The change file that patch-crate
had given me at the end, showed + 1400
and - 600
lines.
All for naught.
Be that as it may, I’ve simply decided to write my own reasonably safe wrappers in time and only use the mlua-sys
crate instead of the mlua
crate for it’s defined lua bindings. I couldn’t use any of the actual mlua
functionality anyway.
Here’s an excerpt from one of the many console output snapshots i took when trying to run those heavily modified mlua
and mlua-sys
versions… for your amusement:
lua closure: pre rotate : 1
first if: 5
managed to lua rotate
checkstack: userdata
lua closure: pushlightuserdata
checkstack: userdata
lua closer: pcall
checkstack: function
lua closure: remove
checkstack: userdata
lua closure: ret 0
xxx
expect passed
running protect lua closure
lua closure: lua_gettop
checkstack: userdata
lua closure: lua_gettop
checkstack: userdata
lua closure: relax limit memory state
checkstack: function
checkstack: function
lua closure: pre rotate: 0
lua closure: pushlightuserdata
checkstack: userdata
lua closure: pcall
checkstack: function
lua closure: remove
checkstackL userdata
lua closure: ret 0
get ref_thread, false
get_gc_metatable : lua_rawgetp
checkstack: table
absindex: -10000
checkstack: userdata
checkstack: 0
this should be something: nil
this should not be true: true
wrapped_failure_mt_ptr
trying to push c func
false
If you really want to, you can probably vaguely see where in the init_from_ptr
function I am by reading this but I wouldn’t recommend trying to wrap your head around this if you’re not proficient in rust and lua
.
Anyway onto my next stupid venture, attempting to convert from a void*
to a function signature in rust.
tl;cbawriting use std::mem::transmute_copy()
with the pointer as an argument
Sadly that didn’t work for the returned functions signatures that I said I could safely ignore at the end of Downfall so whatever we ignore it until it becomes an issue xd
Release?
This is currently on hold until the main project PD2 Heister’s Haptics is finished.
I’ll probably have a bunch of useful, safe, function wrappers for the lua
stuff by then and can consider a release of the plugin template, so I can save people from actually having to write C++
, which is what I consider my mission in life.
Update 2024/08/10
It’s been a while hasn’t it… I’m just here to write an update for this, since I picked the project back up.
Broken
So downloading my repo and trying to compile and load it from a mod… didn’t work.
I have no idea, genuinely, how it ever worked. With what I know now, it makes no sense to me, that I ever got it to work with the old setup.
Remember the thing I talked about right before the Release? paragraph in here? Yeah I have no idea how I managed to fail casting the void*
there to their respective function signatures. It worked just fine this time. In fact it worked so well, that I’ve done it for all of them.
The bigger issue for me was having to deep dive into macros to keep the project somewhat readable. I always kind of sucked at writing rust macros but I feel like I have an… at least decent handle on them now.
Restored
Now I’m sure you’re asking yourself how this all looks like… well let me show you.
The SuperBLT_Plugin_Setup
function has evolved into this:
Now half of this stuff you haven’t seen before but let me explain what’s actually happening here really quickly.
Logging
This part basically takes the string literal (&str
) "pd2_log"
and turns it into a const char*
(*const c_char
in rust). Then It initializes a OnceLock
called PD2HOOK_LOG
by casting the void*
(*mut c_void
in rust), returned by SuperBLT’s lookup function:
payday2-superblt/platforms/w32/plugins/plugins-w32.cpp:42-64
to the expected function signature, which is defined on PD2HOOK_LOG
itself:
Which is just a direct translation of the pd2_log
function found in the same file:
payday2-superblt/platforms/w32/plugins/plugins-w32.cpp:14-35
After all that I can simply use pd2_log the same way they would. Although they don’t really use this pd2_log
function internally but I digress. I then created macros that are usable globally, which use this function in the same way I would use, for example PD2HOOK_LOG_LOG
, in the C++
version of the Native Plugin Template.
Let me show a single example for brevity:
The base of all the logging functions is a macro called PD2HOOK_LOG_LEVEL
, which is only vaguely related to it’s C++
counterpart.
I have to use macros for this, because otherwise I will not get the correct filename and line number from the file!()
and line!()
macros. Since these are expanded in place during compilation.
And here’s PD2HOOK_LOG_LOG
using that base macro:
Without explaining too much how macros work, I’d like to talk about a trick I pulled here to make the Log function properly usable. $($arg:tt)*
is more or less equivalent to C++ VA_ARGS
. The definition of log_message_cstring
basically just takes all arguments passed and treats them the same way the built-in format!()
macro would. I’m sure you can see the resemblance here
This then allows me to use PD2HOOK_LOG_LOG
like this for example:
And have it produce a console output like this:
04:37:25 PM Log: (ExtModDLL src\haptics\connector.rs:53) Connecting to address: ws://127.0.0.1:12345
Panic
This is more or less self explanatory, if you’re aware of what panic::set_hook
does.
Basically, every time the application would panic (more or less equivalent to throwing an unhandled exception), instead of producing a standard error log, it will instead use one of my PD2HOOK_LOG
logging functions to output to the SuperBLT developer console.
panic_info
is just whatever the usual panic message would be.
Function Signature Import
Now for the real meat and potatoes.
Let’s talk about SUPERBLT
first. SUPERBLT
is a struct that holds a HashMap
, basically a key-value pair unsorted list, that associates a function name with a pointer to that function. Now this list starts out empty, obviously and gets filled right here in the Init
function.
This is the basic definition of the SuperBLT
struct.
It also implements Send
because I need to use it across threads later on but let’s ignore all that stuff for now. The only other thing defined here is two functions, to add to this private HashMap
.
Now SUPERBLT
is the same as PD2HOOK_LOG
in the sense that it’s just a globally unique instance of the SuperBLT
struct. It’s defined as follows:
The reason I use a LazyLock
here instead of a OnceLock
is that I need to do the default initialization in place, before trying to insert stuff into it. It also makes accessing the SUPERBLT
instance less messy in code later.
Now SUPERBLT_EXPORTED_FUNCTIONS
is simply an array of string literals (&str
) that contains all of the SuperBLT internal and lua functions that I could possible import. Here’s a very abbreviated version:
Of course I exclude pd2_log
here, because I import it separately, as mentioned in Logging.
Anyway, I iterate over every string here, turn it into a const char*
, then shove it into the function that’s injected by SuperBLT, and save the returned void*
as well as the name of the function itself into the SuperBLT
struct’s HashMap
.
Casting the function pointers
As mentioned before though, I need to cast these pointers to the actual function signature with std::mem::transmute_copy
. There’s many ways to do this. Initially I wrote a ton of functions inside of the impl
block of my SuperBLT
struct, which looked something like this:
But after writing like idk 10 of these I thought
- This is fucking awful I don’t want to write a million of these
- I should cache the cast so I don’t have to cast this every time I call the function
Those two points made me look into rust macros more, to write something similar to what the SuperBLT devs used with the CREATE_NORMAL_CALLABLE_SIGNATURE
pre-processor macro.
After like 3 iterations of this, I present to you the final macro I used:
It takes a func_name
which is self explanatory, a variable number of comma separated parameters defined as param_name: type
via $($param:tt: $ty:ty), *
, and a return type, which is to be defined after a semicolon with the last ; $ret:ty
part.
Now with the help of this macro I was able to simplify my previous function definitions to just this:
()
in rust is equivalent to void
. Although later on i let the macro have $ret:ty
as an optional argument, so that part wasn’t necessary anymore either. Now my impl
for the SuperBLT
struct looks as follows:
Of course there’s a lot more of these but this sure simplified the entire process. Plus the macro would cache all of them in a OnceLock
too so I killed two birds with one stone.
There’s a whole lot of functions that are not exported by SuperBLT though, a lot of them luaL
functions. These I’d have to re-implement myself, but that’s a trivial issue after getting all of this to work.
All that was left now… was to actually get Haptics to work as intended.
The Template will be released once we’re done with Heister's Haptics
and until then we’ll probably be implementing a lot more stuff that isn’t exported. So far it’s the bare minimum.
Thank you for reading the update though, if you did.
I noticed this site is one of the first couple results that come up when you google some specific keywords so, sorry you had to read all of this rambling, but also you’re welcome if you learned something. I hope you were entertained anyway.