Skip to content
 
 

Repository files navigation

crashlink

workflow wakatime PyPI - Version PyPI - Downloads

The pure-Python HashLink bytecode Swiss Army knife.

Warning

This project is under active development. Breaking changes may be made to APIs with zero notice.

Join the Hashlink Modding Community Discord for support!

Features

  • Pure Python with zero dependencies, integrates nicely in a lot of places
  • Deserialisation, disassembly, and (currently incomplete, but usually functional) decompilation of HashLink bytecode
  • Reserialisation and first-class support for patching bytecode assembly
  • A GUI with a graphical disassembler/decompiler, embedded CFG viewer, source-location lookups, and in-place patching
  • A bytecode assembler for creating HashLink bytecode from scratch
  • An HL/C reimplementation to transpile HashLink bytecode straight to C and build it against libhl
  • A full-featured CLI (crashlink) with both one-shot subcommands and a batteries-included interactive REPL (60+ commands!)
  • Xref indexing and source-location mapping
  • (Experimental) Tools to extract debug info from compiled HL/C binaries (PDB/DWARF) back into a navigable bytecode image
  • A scriptable interface for easy integration into other tools

Installation

uvx crashlink # or pip install crashlink

Optionally, install [extras] for a bunch of additional goodies, including a GUI:

uv tool install crashlink[extras] # or pip install crashlink[extras]

Or, for bleeding-edge features, see the Development section.

You also need to have Graphviz installed to generate control flow graphs. On most *nix systems, on Windows (with Chocolatey or Scoop), and on MacOS (with Homebrew), you can install it with your package manager under graphviz.

  • Windows: choco install graphviz
  • MacOS: brew install graphviz
  • Debian: sudo apt install graphviz
  • Arch: sudo pacman -S graphviz
  • Fedora: sudo dnf install graphviz

In order to work with some of the HL/C utilities, you also need to have Hashlink's core packages installed:

haxelib git hashlink https://github.com/HaxeFoundation/hashlink.git master other/haxelib/

Usage

Either:

$ crashlink path/to/file.hl # or python -m crashlink
crashlink> funcs
f@22 static Clazz.main () -> Void (from Clazz.hx)
f@23 Clazz.method (Clazz) -> I32 (from Clazz.hx)
crashlink> fn 22
f@22 static Clazz.main () -> Void (from Clazz.hx)
Reg types:
  0. Void

Ops:
  0. Ret             {'ret': 0}                                       return

Or:

from crashlink import *
code = Bytecode.from_path("path/to/file.hl")
if code.fn(22): # 22 and 240 are typical entry points for the compiler to generate
  print(disasm.func(code.fn(22)))
elif code.fn(240):
  print(disasm.func(code.fn(240)))
# > f@22 static $Clazz.main () -> Void (from Clazz.hx)
# > Reg types:
# >   0. Void
# >
# > Ops:
# >   0. Ret             {'ret': 0}                                       return

CLI basics

Running crashlink <file> with no subcommand opens the file directly and drops you into the interactive REPL (see below), or runs a single command with -c, e.g. crashlink game.hl -c funcs. A few flags are useful here:

  • -N / --no-constants: skip constant resolution on load - helpful for malformed or unusually large files
  • -a / --assemble: treat file as crashlink assembly and assemble it to bytecode (-o sets the output path)
  • -p / --patch: apply a patch module to file (see -o for where to write the result)
  • -d / --debug: enable extra debug output; -t / --traceback: print full tracebacks on error

For everything that doesn't need a live session, there are one-shot subcommands instead - each with its own -h for detailed usage and examples:

$ crashlink funcs game.hl              # list functions
$ crashlink disasm game.hl 42          # disassemble f@42
$ crashlink decompile game.hl 42       # decompile f@42 to pseudo-Haxe
$ crashlink info game.hl               # summary info (version, counts, etc.)
$ crashlink search game.hl "password"  # search strings by substring
$ crashlink db info game.cldb          # inspect a .cldb debug-info database
$ crashlink hlc game.hl --build        # transpile to C and compile it
$ crashlink mcp                        # run as an MCP server

Run crashlink --help-all to print the top-level help plus every subcommand's -h output in one go.

REPL basics

Bytecode objects, functions, and types are addressed by index - f@<findex> for functions/natives, t@<tIndex> for types, g@<gIndex> for globals, s@<index> for strings - the same notation used throughout disassembly output and crashlink assembly. A typical session looks like:

$ crashlink game.hl
crashlink> funcs
f@22 static Clazz.main () -> Void (from Clazz.hx)
f@23 Clazz.method (Clazz) -> I32 (from Clazz.hx)
crashlink> findfunc method          # search by name substring (alias: ff)
f@23 Clazz.method (Clazz) -> I32 (from Clazz.hx)
crashlink> fn 23                    # disassemble a function (alias: f)
f@23 Clazz.method (Clazz) -> I32 (from Clazz.hx)
...
crashlink> decomp 23                # decompile to pseudo-Haxe (aliases: d, pseudo)
...
crashlink> cfg 23                   # render a control flow graph and open it
crashlink> xref func 23             # find every caller of f@23
crashlink> rename 23 0 _ localName  # rename a local for readability
crashlink> save game_patched.hl     # write out your changes
crashlink> exit

help lists every command grouped with its aliases, and help <command> gives its usage and a longer description. Up/down arrows browse command history (persisted across sessions in ~/.crashlink_history), tab completes command names, and clear/history/exit do what you'd expect.

Read the API documentation for more information.

Development

Note

This project is configured for the just command runner and uv. If you don't have them installed, you can still run the commands in the justfile manually and the pip equivalents of the uv commands, but I don't recommend it. At the very least, there's zero downside to switching to uv.

For development purposes, you can clone the repo, install development dependencies, and run the tests:

git clone https://github.com/N3rdL0rd/crashlink
cd crashlink
uv sync --extra dev
just test # or pytest

Before committing, please run just dev to format the code, run tests, and generate documentation in docs/. If you're adding new features to the core serialisation/deserialisation code (core.py), please also add a test case in tests/haxe/ for the new language feature you're adding. If you're adding a feature to the decompiler or disassembler, please add a normal test case (in Python) in tests/ that tests the new feature.

Pull requests are always welcome! For major changes, please open an issue first to discuss what you would like to change.

You can use the following pre-defined commands with just:

  • just dev: Run tests, format code, and generate documentation.
  • just build: Build the package.
  • just install: Install development dependencies and the package in editable mode.
  • just build-tests: Build test samples.
  • just test: Run tests.
  • just format: Format code.
  • just docs: Generate documentation.
  • just check: Run static analysis/typechecking.
  • just clean: Clean up build artifacts.
  • just profile: Run the test suite with cProfile and then open the results in a browser.
  • just serve-docs: Serve the documentation locally.

crashtest CLI

crashtest is a built-in testing system that is used to score the decompiler's output against the original source code. It is used to ensure that the decompiler is working correctly, that the output is correct, that the decompiler is not regressing, and to allow those interested in the project to easily see the state of the decompiler without installing it or running the test suite themselves. You can call it with crashtest auto (or python -m crashtest auto). Make sure you call it from the root of the repository, since it uses relative paths to find the test files and the output directory.

Architecture

Architecture

Roadmap

  • Bytecode parsing
  • Opcode disassembly
    • Local resolution and naming
  • IR lifter (layer 0)
    • If statements
    • Loops
    • Switch opcode statements
    • Function calls
      • CallClosure
    • Closures, lambdas
  • IR optimization layers
    • Resolve locals from assigns block
    • Trace optimization
    • Nested if/else/if/else -> switch
    • More to address issues as they arise!
  • Haxe pseudocode
  • Cross-reference index
  • GUI prerequisites
    • Workspace/project abstraction (wraps Bytecode with cached analysis state)
    • Incremental/async analysis API (background decompile, progress callbacks)
    • Patch buffer (in-memory edits, dirty tracking, re-serialisation)
    • Function search index (by name, file, type)
    • Source location API (debug file + line → function/opcode, and reverse)
  • GUI (probably qt6 at this point)
    • Graphical disassembler
    • Embedded CFG viewer through some Graphviz bindings
    • Decompiler
    • Basic local name patching
    • Other direct patching
    • Rename other symbols
    • Persistent patching/export modified bytecode
    • IR layer viewer
  • Partial recompilation (against stubs of other functions)

Portability

crashlink is written in pure typed Python with a minimum version of 3.10 (for the | operator and match statement). It should run on any modern platform, and has been tested heavily on Windows, Linux, and has been tested, but less heavily, on MacOS. As well as this, it is portable to many Python interpreters:

  • CPython 3.10+ is the main target
  • PyPy also just works
  • IronPython and Jython are not supported due to their earlier Python version targets.
  • RustPython would work, but it doesn't support match statements.
  • Pyodide works and you can see a live demo here

Credits

  • Thank you to Gui-Yom for writing hlbc and for maintaining documentation on the HashLink bytecode format, as well as for providing tests and helping me during development.
  • Thank you to Haxe Foundation for creating the HashLink VM and the Haxe programming language.
  • Thank you to the Dead Cells community on Discord for providing me with the motivation to start this project.
  • And a big thank you to you, dear user, for being at least partially interested in this project.

❤ N3rdL0rd

About

Pure-Python HashLink bytecode Swiss Army knife.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages