Languages.
VeilCC works on compiled objects and LLVM IR, not source, so anything that lowers through LLVM is in reach. Below is the exact path for C/C++, Rust, and C# through Unity IL2CPP.
The integration model
Every language follows the same two-step shape. You never hand us source.
- Name the crown jewel. Either annotate it in source (C/C++ and Rust can), or name its symbol at build time (any language). A symbol you name must have the C ABI so it survives with a stable name.
- Run the pass on the object or IR. The console, CLI, or GitHub Action sends the compiled object (sealing) or LLVM IR (obfuscation) to the Protection API; you link the returned artifact exactly as before.
Two inputs, two tiers. Obfuscation (veil:mba, veil:flatten, veil:poison, veil:vm, or the veil:max bundle) rewrites LLVM IR in place. Sealing (veil:seal) takes the compiled object and ships the function as ciphertext. See Security levels.
C / C++
The most direct path: annotate in source, build as usual, swap the object. Auto-discovery means you don't repeat yourself, mark the function and the pass finds it.
1 · Annotate the function
// Seal: ships as ciphertext, decrypted only at call time.
__attribute__((annotate("veil:seal")))
uint32_t check_license(const char *key) {
/* your real algorithm */
}
// Obfuscate only (hot-path-safe, stays native): the max bundle
// = poison + mba + flatten with a fresh seed each build.
__attribute__((annotate("veil:max")))
int validate_token(const char *tok, size_t n) { /* … */ } 2 · Build, protect, link
clang -O2 -c license.c -o license.o # build as you always do veil protect --level seal license.o -o license.sealed.o clang -O2 app.o license.sealed.o -o app # link, nothing else changes
C++ works identically. Give the sealed function extern "C" so its symbol keeps a stable, un-mangled name, or pass the mangled name to --proto. Everything that isn't marked stays untouched and native.
Rust
Rust lowers through LLVM, so the same pass applies. The one requirement is a stable symbol: export the crown jewel with the C ABI.
1 · Export with the C ABI
#[no_mangle]
pub extern "C" fn check_license(key: *const u8, len: usize) -> u32 {
let key = unsafe { std::slice::from_raw_parts(key, len) };
// your real algorithm
} #[no_mangle] + extern "C" give the function a predictable name and calling convention, exactly what the sealer needs to lift it out and the loader needs to call it back.
2 · Seal the compiled object
Rust has no annotate attribute, so you name the symbol and its signature in C terms. (A #[veil::protect] helper macro that emits the annotation section is on the roadmap; naming the symbol works today.)
# produce an object (staticlib, or cargo's emitted objects)
cargo build --release
# seal by symbol + C prototype
veil protect --level seal \
--symbols check_license \
--proto "uint32_t check_license(const unsigned char*, size_t)" \
target/release/liblicense.o -o license.sealed.o For the obfuscate tier, emit IR with cargo rustc --release -- --emit=llvm-ir and target the function by name; the pass hardens the IR before lowering. Keep the crown jewel in its own crate/unit so its object is small and clean to seal.
C# via Unity IL2CPP
IL2CPP transpiles your C# to C++ and then to native code, which is where VeilCC meets it, the same object path as C. There are two ways in; the first is cleaner and the one we recommend.
Recommended · Protect a native plugin
Move the logic worth stealing, the license check, the anti-cheat core, the proprietary kernel, into a small native plugin (C/C++ or Rust), call it from C# over P/Invoke, and seal the plugin. The secret never exists in managed IL or in the IL2CPP output at all.
// C#: call the native crown jewel
static class LicenseNative {
[DllImport("license")] // license.dylib / .so / .dll
public static extern uint check_license(string key);
}
// license.c: the sealed function (built + sealed like any C object)
__attribute__((annotate("veil:seal")))
uint32_t check_license(const char *key) { /* … */ } Build the plugin, seal it with veil protect --level seal, and drop it into Assets/Plugins/. Device-bound keys (secure enclave / TPM) are available here, which matters most for games and anti-cheat.
Advanced · Target the IL2CPP output directly
If the logic must stay in C#, protect the generated native code. Point IL2CPP at a C++ build, then protect the compiled objects by symbol before the final link.
- Set the IL2CPP build to emit and keep its generated C++ (
Il2CppOutput/) and its object files. - Name the generated symbol for your method, IL2CPP names it deterministically per build, or force it by name at protect time.
- Seal or obfuscate that object, then let Unity finish the link.
Honest caveat: IL2CPP symbol names can shift between builds and Unity versions, so this path needs a name check wired into your build. C# via IL2CPP runs the same object path we verify for C, but we only mark a toolchain as verified once our suite proves it end-to-end; today it's listed as expected. The native-plugin route above is verified C and is what we'd ship on.
The C-ABI rule
One rule spans every language: a function you seal or name must present the C ABI.
- Why. The sealer lifts the function's machine code and relocations out of the object and the loader calls it back by a stable symbol with a known calling convention. C++ overloads, Rust's default mangling, and generics don't give that.
- How.
extern "C"(C++),#[no_mangle] pub extern "C"(Rust), a plain exported C function (plugins). Pass the signature to--protoin C types. - Scope. Only the sealed boundary needs the C ABI. Everything it calls, internal helpers, your normal C++ or Rust, stays as-is and links normally.