George Huebner

disaster.el + zig cc = Budget godbolt

Disaster is an Emacs package authored by the venerable Justine Tunney that allows disassembling C/C++ and Fortran code from the comfort of your source editing buffer. I really like this idea, but most of the time I’m interested in x86_64 assembly instead of my aarch64 host platform, so I decided to hack on cross compilation support using Zig as a C/C++ cross compiler!

I’m using disaster because its simplicity fits my use case, but you should be able to use the same idea with other packages like

RMSbolt
Has support for tons of languages (and bytecode, not just assembly), and you can track point across source and compilation buffers
Beardbolt
Rewrite of RMSbolt that is faster/simpler than RMSbolt, but only supports C/C++/Rust

PoC Or GTFO

(use-package disaster
    :config
  (setq disaster-cflags (setq disaster-cxxflags "-Wno-everything -g"))
  (setq disaster-assembly-mode 'nasm-mode)
  :init
  (define-advice disaster (:around (fn &rest args) cross-compile)
    (interactive)
    ;; OPTIONAL: support for non-file buffers
    (setq arg (or args
                    (list (if-let* ((buf (current-buffer))
                                    (buf-name (buffer-name buf))
                                    (file (buffer-file-name (current-buffer))))
                              file
                            (make-temp-file
                             (file-name-base buf-name) nil
                             (cond ((eq major-mode 'fundamental-mode) (c-or-c++-ts-mode))
                                   ((member major-mode '(c-mode c-ts-mode)) ".c")
                                   ((member major-mode '(c++-mode c++-ts-mode)) ".cpp")
                                   ((eq major-mode 'fortran-mode) ".f")
                                   (t (file-name-extension buf-name)))
                             (buffer-string)))))
          ;; replace `doit` with `apply fn args` if you get rid of this
          doit (lambda () (if args (apply fn arg)
                            (with-temp-buffer (apply fn arg)))))
    ;; END-OPTIONAL
    (if (and current-prefix-arg (mapc (lambda (exe)
                                        (or (executable-find exe) (user-error "disaster: %s not found" exe)))
                                      '("zig" "jq")))
        (let* ((monch (lambda (prompt collection default)
                        (completing-read prompt (split-string collection " ") nil nil nil nil default)))
               (file-exists-wrapped (symbol-function #'file-exists-p))
               (targets (split-string (shell-command-to-string "zig targets | jq -r '.arch,.os,.abi | join(\" \")'") "\n" t))
               (host-target (mapcar (lambda (s) (car (split-string s "[ \.\t\n\r]+"))) (split-string (shell-command-to-string "zig env | jq -r '.target'") "-")))
               (target-arg (apply #'format " -target %s-%s-%s" (cl-mapcar monch '("Arch: " "OS: " "ABI: ") targets host-target)))
               (disaster-cc "zig cc")
               (disaster-cxx "zig c++")
               (disaster-cflags (concat disaster-cflags target-arg))
               (disaster-cxxflags (concat disaster-cxxflags target-arg)))
          (with-environment-variables (("CC" disaster-cc)
                                       ("CXX" disaster-cxx)
                                       ("CFLAGS" disaster-cflags)
                                       ("CXXFLAGS" disaster-cxxflags))
            (cl-letf (((symbol-function #'file-exists-p)
                       (lambda (file)
                         (unless (string= "compile_commands.json"
                                          (file-name-nondirectory file))
                           (funcall file-exists-wrapped file)))))
              (funcall doit))))
      (funcall doit))
    ;; OPTIONAL: Put point in assembly buffer
    (switch-to-buffer-other-window disaster-buffer-assembly)))

This requires zig and jq to be in Emac’s exec-path, although I’m sure you could use Elisp to do the JSON parsing instead, especially now that Emacs 30.1 ships with its own JSON implementation. Most sane people would object to my usage of cl-letf and advice instead of a separate wrapper function; this would probably be more readable as a patch instead, but I like having a snippet that you can quickly try out with eval-last-sexp.

Caveat emptor: this falls apart for large projects, poorly behaved Makefiles, and probably CMake (I tried ameliorating the latter issue upstream, but I don’t use CMake that often so YMMV). Also, this definitely doesn’t count as a “Compiler Explorer” in the strict sense because you’re using the same version of LLVM regardless of target; you might be able to do something like leverage nixpkgs’ cross compilation support to build older cross-compilers, but you’re probably better off using Docker or Godbolt at that point.

#emacs