Files
nethack/sys/libnh
Benjamin de WaalandClaude Opus 4.7 f6afa23e28 libnh: fix native build of libnh.a on macOS
Fixes four issues that prevented `make WANT_LIBNH=1 all` from producing
a libnh.a that could be linked into a host program on macOS.  Before
these patches, it built but the resulting archive was unusable: macOS
ld errored on a nested liblua archive member, was missing date.o and
hacklib symbols (`populate_nomakedefs`, `eos`, `lcase`, `mungspaces`,
...), and had duplicate definitions of `main`, `whoami`, etc.

Specific changes:

1. sys/libnh/libnhmain.c: drop `static` on `whoami()`.  src/earlyarg.c
   declares it `extern` and calls it from `scores_only()`; the static
   makes it file-local and that reference goes unresolved.

2. sys/libnh/libnhmain.c: gate the emscripten-only code in get_nhuuid
   with `#ifdef __EMSCRIPTEN__` instead of `#ifdef NHUUID`.  The macOS
   hints define NHUUID for the libnh build (they did so unconditionally
   before NO_NHUUID even existed), so on native builds the compiler
   tried to call `emscripten_run_script_int` / `_string` and failed
   with implicit-function-declaration errors.  __EMSCRIPTEN__ is the
   real signal for "this is being cross-compiled to WASM."

3. sys/unix/hints/macOS.500: in the WANT_LIBNH block, add an explicit
   `recover: lua_support` dependency (gated by MAKEFILE_TOP).  When
   $(GAME) is overridden to empty, the regular `recover: $(GAME)` chain
   no longer triggers `lua_support`, so include/nhlua.h never gets
   generated and recover.c's transitive #include of hack.h fails.

4. sys/unix/hints/macOS.500: rewrite the libnh.a rule.  The previous
   `ar rcs libnh.a $(HOBJ) $(LIBNHSYSOBJ) liblua-$(LUA_VERSION).a` had
   four problems: (a) ar archives liblua.a as a single opaque member
   that macOS ld can't dereference, (b) date.o (DATE_O, kept separate
   from HOBJ) was never archived, so `populate_nomakedefs` and
   `nomakedefs` were missing, (c) hacklib.a was likewise omitted, and
   (d) HOBJ already contains $(SYSOBJ) (with unixmain.o) and $(WINOBJ)
   (the tty windowport), which duplicated symbols from libnhmain.o /
   winshim.o.

   The fix uses `libtool -static` so hacklib.a and liblua's archive
   have their members merged rather than nested, depends on $(LUALIB)
   so lua_support runs first, includes $(DATE_O) and $(TARGET_HACKLIB),
   and uses $(filter-out $(SYSOBJ) $(WINOBJ),$(HOBJ)) to drop the
   duplicates.

Verified by clean rebuild on macOS 26 (arm64, Apple clang 17):
  make spotless
  make fetch-Lua
  make WANT_LIBNH=1 all
and link-tested with a tiny harness that calls
shim_graphics_set_callback() against the resulting libnh.a.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 07:06:41 +01:00
..
2026-04-26 11:08:10 -04:00
2026-04-26 11:08:10 -04:00
2026-04-26 10:47:48 -04:00

About

This creates a library for NetHack that can be incorporated into other programs. There are two different libraries that are currently available:

  • libnethack.a - a binary Unix library
  • nethack.js / nethack.wasm - a WebAssembly / WASM library for use in JavaScript programs (both nodejs and browser)

Build

This library has only been built on MacOS, but should work on Linux and other unix-ish platforms. If you have problems, start by stealing hints files from the sys/unix/hints for your platform. Contributions for other platforms are happily accepted.

Building the WASM module requires that you have the emscripten toolchain / sdk installed.

Generally the build is the same as the unix build:

[Edit Oct 4, 2020: Use the existing Makefile and hints, hints/include system for cross-compiles]

  1. cd sys/unix
  2. ./setup.sh hints/macOS.500
  3. cd ../..
  4. For libnethack.a: make WANT_LIBNH=1 all
  5. For nethack.js: make CROSS_TO_WASM=1 all

[Original text was:]

  1. cd sys/lib
  2. For libnethack.a: ./setup.sh hints/macOS.500; for nethack.js: ./setup.sh hints/wasm
  3. cd ../..
  4. make

[Edit Oct 4, 2020:] Resulting libraries will be in the targets/wasm directory for CROSS_TO_WASM=1. Resulting libraries will be in the src directory for WANT_LIBNH=1.

[Original text:] Resulting libraries will be in the src directory.

WASM also has a npm module that can be published out of sys/lib/npm-library. After building the nethack.js it can be published by:

  1. cd sys/lib/npm-library
  2. npm publish

API: libnethack.a

The API is two functions:

  • nhmain(int argc, char *argv[]) - The main function for NetHack that configures the program and runs the moveloop() until the game is over. The arguments to this function are the command line arguments to NetHack.
  • shim_graphics_set_callback(shim_callback_t cb) - A single function that sets a callback to gather graphics events: write a string to screen, get user input, etc. Your job is to pass in a callback and handle all the requested rendering events to show NetHack on the screen. The callback is void shim_callback_t(const char *name, void *ret_ptr, const char *fmt, ...)
    • name is the name of the window function that needs to be handled
    • ret_ptr is a pointer to a memory space for the return value. The type expected to be returned in this pointer is described by the first character of the fmt string.
    • fmt is a string that describes the signature of the callback. The first character in the string is the return type and any additional characters describe the variable arguments: i for integer, s for string, p for pointer, c for character, v for void. For example, if format is "vis" the callback will have no return (void), the first argument will be an integer, and the second argument will be a string. If format is "iii" the callback must return an integer, and both the arguments passed in will be integers.
    • Variadic arguments: a variable number and type of arguments depending on the window function that is being called. The arguments associated with each name are described in the NetHack window.txt.

Where is the header file for the API you ask? There isn't one. It's three functions, just drop the forward declarations at the top of your file (or create your own header). It's more work figuring out how to install and copy around header files than it's worth for such a small API. If you disagree, feel free to submit a PR to fix it. :)

API: nethack.js

The WebAssembly API has a similar signature to libnethack.a with minor syntactic differences:

  • main(int argc, char argv[]) - The main function for NetHack
  • shim_graphics_set_callback(char *cbName) - A String representing a name of a callback function. The callback function be registered as globalThis[cbName] = function yourCallback(name, ... args) { /* your stuff */ }. Note that globalThis points to window in browsers and global in node.js.
    • name is the name of the window function that needs to be handled
    • ... args is a variable number and type of arguments depending on the window function that is being called. The arguments associated with each name are described in the NetHack window.txt
    • The function must return the value expected for the specified name

API Stability

The "shim graphics" API should generally be stable. I aspire to replace the command line arguments (argc / argv) with a structure of options, so the nhmain() and main() functions may change at some point.

libnethack.a example

#include <stdio.h>

int nhmain(int argc, char *argv[]);
typedef void(*shim_callback_t)(const char *name, void *ret_ptr, const char *fmt, ...);
void shim_graphics_set_callback(shim_callback_t cb);

void window_cb(const char *name, void *ret_ptr, const char *fmt, ...) {
    /* TODO */
}

int main(int argc, char *argv[]) {
    shim_graphics_set_callback(window_cb);
    nhmain(argc, argv);
}

nethack.js example

const path = require("path");

// starts nethack
function nethackStart(cb, inputModule = {}) {
    // set callback
    let cbName = cb.name;
    if (cbName === "") cbName = "__anonymousNetHackCallback";
    let userCallback = globalThis[cbName] = cb;

    // Emscripten Module config
    let Module = inputModule;
    savedOnRuntimeInitialized = Module.onRuntimeInitialized;
    Module.onRuntimeInitialized = function (... args) {
        // after the WASM is loaded, add the shim graphics callback function
        Module.ccall(
            "shim_graphics_set_callback", // C function name
            null, // return type
            ["string"], // arg types
            [cbName], // arg values
            {async: true} // options
        );
    };

    // load and run the module
    var factory = require(path.join(__dirname, "../build/nethack.js"));
    factory(Module);
}

nethackStart(yourCallbackFunction);