Working with FFI
With Enable FFI checked, LuaJIT ffi is loaded into the script env. Realistically:
Reasonable / power-user
Section titled “Reasonable / power-user”- Declare and call exported C APIs from libraries already mapped in the CS2 process (
libc, etc.) when you know the symbol and ABI. - Pack/unpack binary layouts you reverse yourself (structs, bitfields) for one-off experiments.
- Pointer arithmetic on addresses you already obtained from a trusted path (e.g. a value you printed from schema / a module base you resolved carefully).
- Performance-sensitive math or bulk buffer walks without crossing into C++ bindings for every field.
Hard / fragile without more C++ help
Section titled “Hard / fragile without more C++ help”- Calling C++ member functions / virtual methods (thiscall, vtables, name mangling, Itanium ABI on Linux).
- Finding reliable module bases of
libclient.sowithout a binding (aquadoes not currently exposedl_iterate_phdrhelpers). - Anything that must stay VAC/thread-safe - wrong-thread calls and bad pointers crash the game, not just the script.
Strongly discouraged
Section titled “Strongly discouraged”- Writing random process memory “because it might be a cvar”.
- Loading extra
.sofiles from disk (package.loadlib/ffi.loadof untrusted paths). - Long-lived
fficallbacks from engine threads you do not own.
Prefer first-class bindings instead of FFI when possible
Section titled “Prefer first-class bindings instead of FFI when possible”If many scripts need the same thing (input, items, steamId, weapons), it should be a normal aqua.* API - safer, versioned, and no ABI guesswork. FFI is the escape hatch for advanced authors, not the default toolbox.
Example shape (illustrative only - still requires correct addresses/types):
-- Requires Enable FFIlocal ffi = require("ffi")ffi.cdef[[ int puts(const char *s);]]-- ffi.C.puts("hello from ffi") -- libc, usually present