Emacs Plugins in OCaml: Hello, Ecaml! (part 1)

There was recently a thread on the OCaml Discourse forum about sample Emacs plugins using the Ecaml library. Since the library doesn’t seem to have been getting that much use, and since I have some personal interest1 in seeing it used more, I decided to help by writing a Getting Started guide. Here goes.

This is part 1. We’ll see why you might use Ecaml to write a plugin, and get it set up and running with a simple Hello World. All of the code for this series is available on GitHub.

(This guide has only been tested on macOS 10.12, but probably works on other Unices like Linux or OS X. Unfortunately I have no idea about Windows.)

1 Emacs

Emacs is a great text editor operating system habitat. Emacs’s power and popularity arguably stem from its ability to be endlessly customized and enhanced by plugins (i.e., extensions, packages, modules)—for example, I’m writing this post in org-mode while editing its YAML metadata (in the same file) in yaml-mode. These two editing modes are able to play nicely in the same buffer thanks to a package called mmm-mode (“multiple major modes”… mode).

In this series, we’re going to write an interpreter for a minimalist Turing-complete programming language. We want to be able to run the code we’re editing directly within Emacs, instead of switching to our shell and running it from there, so we’ll embed our interpreter as a plugin in Emacs.

1.1 Elisp

In the past, Emacs plugins have all been written in a language called Emacs Lisp (or Elisp), which, as its name suggests, is a Lisp. Elisp is a perfectly nice language, especially for writing the sort of code you might find in a plugin, but it has a couple of drawbacks. The main one I’m interested in is speed.

Ya see, Emacs’s Lisp interpreter ain’t exactly the fastest. A na├»ve speed comparison between Elisp and Ruby, for dramatic effect:

(benchmark-run (print (loop for i below 1000000 sum i)))
;; 499999500000
;; (0.389676 0 0.0)
time ruby -e 'p (0...1000000).reduce(&:+)'
# 499999500000
# ruby -e 'p (0...1000000).reduce(&:+)'  0.17s user 0.03s system 81% cpu 0.236 total

Both of these snippets just iterate from 0 to 999,999, adding up

The Elisp code takes quite a bit longer to add up the natural numbers less than one million, and that’s including the startup time of the ruby interpreter! Now, to be fair, when compiled, the above Elisp code only takes around 0.125 seconds, but that’s still substantially slower than compiled languages like C or Java (or OCaml).

1.2 Native Plugins

Emacs 25 added the ability to load native code plugins. Native code refers to code that runs directly on the processor, instead of being interpreted by another program, like Emacs’s Lisp interpreter or Ruby’s virtual machine.

There’s quite a good introduction to the basics of writing native plugins here, but the gist of it is that Emacs exposes a small C API which you can write modules against. Compiling them to shared objects (i.e., dynamically linked libraries, .so’s, .dll’s, etc.) allows Emacs to dynamically load the plugin at runtime.

2 OCaml plugins?

OCaml is a statically-typed functional programming language with a great deal of versatility, contrary to what you might have first expected when you just read the words “statically-typed” and “functional”. Of course, since you’re reading this blog post, I assume you are familiar with OCaml and won’t belabor singing its praises.

I’ll just point out that OCaml has a quite nice way to interface with C code using its foreign function interface (FFI), which means we can use OCaml to write native plugins for Emacs, as long as we have some library to provide the interface between the Emacs C API and our OCaml plugin. That’s where Ecaml comes in.

3 Let’s write a plugin

Ecaml is an open-source OCaml library for writing native Emacs plugins, available on OPAM or Jane Street’s GitHub page. We’ll use Ecaml to write our Emacs plugin, in OCaml instead of Elisp.

mkdir ~/ecaml-bf
cd ~/ecaml-bf
git init           # or hg, if you prefer
touch jbuild-workspace

3.1 Obtaining Ecaml

We’ll use the development version of Ecaml, since the version in the OPAM repository is significantly outdated. Jane Street hosts an OPAM repo with the development versions of their open-source libraries, so let’s add the repo to OPAM:

opam switch 4.05.0
opam repo add janestreet-bleeding https://ocaml.janestreet.com/opam-repository
opam install ecaml

3.2 Hello from OCaml

We’ll start with Hello World for now (src/plugin.ml):

let () =
  Ecaml.message "Hello from OCaml";
  Ecaml.provide (Ecaml.Symbol.intern "ecaml-bf")
;;

Our simple plugin just writes a message to the echo area (or standard error if Emacs is run in --batch mode). Emacs plugins need to provide the name of the plugin in order to be loaded successfully by require, so we do that just before our plugin exits.

Note that OCaml’s module initialization (i.e., the stuff that runs when you compile and execute a normal OCaml program) serves as the code that is run when the plugin is loaded. In order to extend Emacs’s behavior while running, we’ll need to define some Elisp functions and key bindings to call those functions, later. For now, let’s just try our plugin out.

3.3 Compiling our plugin

Unfortunately, I haven’t yet been able to figure out a way to get jbuilder to build shared objects. However, taking advantage of the fact that executable files and shared objects have basically the same file format (e.g., Mach-O or ELF), we can just pretend that the compiled executable is a shared object. It’s fine, trust me. Probably.

We’ll set up our jbuild file like so:

(jbuild_version 1)

(executables
 ((names     (plugin))
  (libraries (ecaml))))

(rule (copy plugin.exe ecaml-bf.so))

(alias
 ((name plugin)
  (deps (ecaml-bf.so))))

(alias
 ((name runtest)
  (deps ((alias plugin)))
  (action (run emacs -Q -L . --batch --eval "(require 'ecaml-bf)"))))

Note the copy rule which simply copies the ocamlopt-produced native executable file to another name ending in .so. This is the filename extension that Emacs will look for when we try to load our plugin.

We’ll also define a runtest action that will run Emacs in batch mode and require our plugin. (I added the -Q flag to skip user initialization, etc. No need to load all of my Spacemacs config just to run a small test in batch mode!). Let’s try it out:

jbuilder build @plugin 2>/dev/null && jbuilder runtest
# Hello from OCaml
# "Loaded Ecaml."

As you can see, the plugin printed out our message to standard error. It also printed out a message from Ecaml itself. Normally these messages just end up in your *Messages* buffer, so it’s easy to check to see that they’re there.

Great! We successfully compiled and loaded our plugin into Emacs, without writing a lick of Elisp (except for the (require 'ecaml-bf)). Next time we’ll start working on our interpreter. Stay tuned!

Footnotes:

1

I wrote much of an earlier version of Ecaml2 (e.g., the C stubs and low-level OCaml interface) during my internship at Jane Street in the summer of 2016 under the tremendous guidance of my mentor Stephen Weeks. It’s a great place to work, and I recommend anyone interested in their internship program to definitely check it out and/or apply.

2

Of course, it has since been much expanded, rewritten, and improved upon by people much more experienced than me.

Share

Comments