FatigueDev

FatigueDev

NIF like a champ: VS Code Debugging C/C++ NIFs

Preface ~ NIFs are rough as heck to debug properly.

So you’ve created a NIF with a decent amount of complexity and have resorted to using std::cout << "Some value is: " << value << std::endl;, only to get a Segfault because the line four lines up is the one causing the problem. Situations like this if you’ve been creating NIFs before are pretty common, but this guide is going to help you get set up and tripping breakpoints without destroying your BEAM VM.

~ Important Links ~

Installing Erlang OTP
Debugging NIFS
NIF Overview
ERL_NIF C Reference

Windows users, please read this:
The binary install of Erlang for Windows comes with a prebuild debug compilation! You can skip all the way to the VS Code & Breakpoints (Finally, Jeeze) section. The command you need to run, however, is going to omit -emu_flavour emu; instead run iex.bat --erl '-emu_type debug' -S mix. Still target the available debug.beam you have, however. Thanks, @garazdawi! (ref: corrections by garazdawi)

Basically:

In short, we need to compile a debug virtual machine.
We also need to ensure we have the required utilities to actually run these scripts and compile, too.

Getting the right version

Determine your current version of Erlang by running:
erl -eval 'erlang:display(erlang:system_info(otp_release)), halt().' -noshell
image

Once it spits out your number, we can use the latest version under the 27 tag. In my case, 27.1.2.

Then, head to the Erlang OTP github and find the name of the tag you should be using by filtering by tags.

You can either download as a ZIP at that point and unpack it somewhere we’re going to compile it, or run:

# Clone our selected tag to a new folder named OTP-27.1.2
git clone --depth 1 --branch OTP-27.1.2 https://github.com/erlang/otp.git OTP-27.1.2

Preconfigure and configure

Open up the folder in your preferred IDE. In the root directory of OTP-27.1.2, set a local variable used for the configure application called ERL_TOP, which is simply the full path to the directory of what we just cloned.
export ERL_TOP=pwd

You should know you set it correctly if you echo $ERL_TOP and it spits out your current directory as a full path.

Compiling is slow as heck without parallelism, so we’ll also set MAKEFLAGS as well:
export MAKEFLAGS=-j8

Now for when we’re installing, it’s going to require that we set up a prefix. This defaults to /usr/local and has to be an absolute path. What this means for Windows and Linux devs is that to set up where we eventually want it to go, we need to set it as one of our first things before we even compile. So we’ll run configure, an application within our cloned root like this:

./configure --prefix=/usr

Below is an image of configuring for the default /usr/local prefix. It’s fine, copy paste still works if we messed this up (like I did while writing :smirk:)

It’ll spit out a bunch of checking this, ensuring that; you’ll know it’s fine as long as it doesn’t fail somewhere; in which case it’ll tell you it failed.

Compiling Erlang With Debug Enabled

Now that we’re configured, we have to first run:
make

This will do a pass and just build our typical OTP version which is fantastic. Once it’s done (should be much faster thanks to setting MAKEFLAGS=-j8), we need to specify that we’d like to create some Types and Flavours. It supports a wide range of tooling and you can find the specific type/flavour requirements here.

While we only need one, the more the merrier. Let’s run:
make TYPE=debug FLAVOR=emu

After that compiles,
make TYPE=debug

Once those finish up, we can verify that it worked as intended by simply checking if we’ve got some of these beam.debug. files in the ./bin/(architecture)/ folder:

And running the generated erlang with:
./bin/erl -emu_type debug -emu_flavor emu

Installation

We can test first by specifying we want to install to a directory inside our cloned project, just to make sure it looks fine. Of course, if we missed setting the --prefix during configuration we can set it in the Makefile at ./Makefile.

Install to a local directory to make sure it all looks fine:
mkdir install_test && make DESTDIR=$ERL_TOP/install_test install

Additionally, we’ll want to install for our debug type and flavours as well:
make DESTDIR=$ERL_TOP/install_test install TYPE=debug
make DESTDIR=$ERL_TOP/install_test install TYPE=debug FLAVOR=emu

Let’s install to the proper directory when we’re sure it all looks gucci with:
sudo make install

Important! Next we install our debug targets as well:
sudo make install TYPE=debug
sudo make install TYPE=debug FLAVOR=emu

Making sure our install worked

Now, if everything went as to be expected, we can run erl like we did earlier- Except with the Erlang we have properly installed.

Open a new terminal context to refresh the context and run:
erl -emu_type debug -emu_flavor emu

Additionally, we should be able to pass forward those erl flags to iex with:
iex --erl '-emu_type debug -emu_flavor emu'

We can triple check with:

iex> :erlang.system_info(:emu_type)
:debug
iex> :erlang.system_info(:emu_flavor)
:emu

We should see it’s got [type-assertions] [debug-compiled] [lock-checking] in our Erlang, :debug and :emu as our type and flavour. Which is fantastic!

If not, I refer you to the actual documentation where you can give it another go with the actual docs.

VS Code & Breakpoints (Finally, Jeeze)

So according to the docs for the Debug Emulator, connecting GDB to this should work out of the box. Assuming you have a project that already has a NIF in it, we’ll begin by finding the right extensions for the job.

Microsoft’s C++ - GDB Attach
Process Matcher - Absolute Time Saver; Stamp PID match

Those two tools are going to be the ones we’re going to leverage to attach to our running IEX session. So now we’ll head to project_directory/.vscode/ and make launch.json in there if it doesn’t exist already and chuck in:

{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "(GDB) Attach",
      "type": "cppdbg",
      "request": "attach",
      // Program is a fully qualified path to a program to debug.
      // Fortunately for us, whatever is running in beam.debug.emu
      // also has all of our loaded symbols, or _will_ have our loaded
      // symbols when we call :erlang.load_nif/2
      "program": "/usr/lib/erlang/erts-15.1.2/bin/beam.debug.emu",
      "processId": "${input:matchDebugBeam}",
      "internalConsoleOptions": "openOnFirstSessionStart",
      "MIMode": "gdb"
    },
  ],
  "inputs": [
    {
      "id": "matchDebugBeam",
      "type": "command",
      "command": "process-matcher.match",
      // Program in here just needs to be the name of what's running.
      // Spin up iex with debug and make sure it's name matches what's in here.
      "args": { "program": "beam.debug.emu" }
    }
  ]
}

Note: Unfortunately, I haven’t as of yet found a way to allow us to attach without requiring administrative priviledges. That means that each time you hit F5 and try to attach, you’ll hit an auth wall which is irritating. I’m sure we can launch IEx in debug mode using a different launch target and attaching it like that, but this is the most reliable way I’ve found to make this happen.

Running the debugger after starting debug IEX we see the following:

Once we authenticate, we can see that we’ve got a whole heap of processes! If you aren’t running iex in debug, you’ll know it because it will be pause/break on a thread somewhere; unable to continue unless the debugger is detached and you restart iex.

Now assuming you’ve compiled your NIF with debug symbols embedded within it with -g in gcc, clang or whatever other compiler flavour you use (Debug and not Release on Windows, I believe), when your NIF is called and the debugger in VS Code is attached gdb should trip the debugger when it hits a breakpoint set within a C or C++ file.

Observe, in this terribly choppy imgur-compressed gif which hurts my eyes:

Breakpoints in action

In any case, it should make actually debugging your NIFs a bit easier now you can hit breakpoints. You’ll still Segfault of course, but now you’ll at least have a better way of figuring out the reason by walking up the call stack and just having a look around.

Example Source:

hello.cpp
#include <iostream>
#include <erl_nif.h> // Ensure you link NIF headers! Check source for existing NIFs for examples

int main() {    
    return 0;
}

static ERL_NIF_TERM check(ErlNifEnv *env, int argc, const ERL_NIF_TERM *argv) {
    int a = 5;
    int b = a + 5;
    return NULL; // Let's cause a crash, just to test! :)
    return enif_make_atom(env, "ok");
}

static ErlNifFunc nif_funcs[] = {
    {"check", 0, check, 0}
};

ERL_NIF_INIT(Elixir.Example.Nif, nif_funcs, NULL, NULL, NULL, NULL)
nif.ex
defmodule Example.Nif do
  @moduledoc false

  @nifs [check: 0]
  @on_load :onload

  def os_specific_load_path(file_name) do
    {os_family, _os_name} = :os.type()

    if os_family == :unix do
      # Change this to your shared object folder.
      to_string(Path.expand("priv/cmake_example/" <> "lib" <> file_name))
    else
      # Change this to your shared object folder (For windows, it'd just be what it already is + /Debug/ )
      to_string(Path.expand("priv/cmake_example/Debug/" <> file_name))
    end
  end

  def onload do
    # Add the name of your compiled shared object file here :)
    file = os_specific_load_path("cmake_example")
    :erlang.load_nif(file, 0)
  end

  def check, do: :erlang.nif_error("Not loaded.")
end

Summary

I’m glad I finally got this working; hope this helps aspiring NIF creators or those that already hit their heads against enif to have a much nicer development experience.
tl;dr:

  • Compile & Install Erlang Debug
  • Compile your C/C++ to a shared object (.so, .dll) with -g debug symbols
  • Create a launch.config configuration using that debug compiled Erlang
  • Run iex using the debug emulator, run the debug launch config to attach the debugger
  • Hit breakpoints like a champion
  • Segmentation fault: core dumped

Edit: Replaced the important links with the actual correct links (can’t edit while post is pending)
Edit 2: Actual Erlang Core team member @garazdawi saying nice post was awesome, thanks for helping me cut out and correct the steps!
Edit 3: Generally cleaned up a little :slight_smile:

Most Liked

garazdawi

garazdawi

Erlang Core Team

Nice guide!

FYI you can run “make install TYPE=debug” and it should install all the debug files your want.

Windows users can run that just fine in WSL, which the entire Erlang/OTP build system is centered around. They don’t need to though as the windows installer is shipped with beam.debug.smp prebuilt, so Windows users can just run erl.exe -emu_type debug and they will get a debug emulator.

FatigueDev

FatigueDev

Thanks for the reply! I’ll update the guide shortly. I was also wondering if I could cross post this to the Erlang forum since the major point of it is making Erlang hook properly to VS Code. Not sure if it’d be allowed or not given it’s current format is quite Elixir-y.

^ Would’ve cut the guide down to 1/4 of the size; amazing stuff!

Where Next?

Popular in Guides/Tuts Top

malloryerik
Hi, this is for people who, like me, have had some friction using .html.heex templates in VSCode. The solution seems to be, in a hyphena...
New
benwilson512
Correct if me I’m wrong, as best I can tell there aren’t any reasons to use mix run --no-halt in production vs releases. The marginal val...
New
bluegene
Hi guys, I’ve been on a personal journey to learn Elixir for the past two years. During this journey I’ve been using the spaced repetiti...
New
WolfDan
So my main OS is Windows, I do must of my work with it, Elixir and vscode elixirls works just fine when you’re working only with elixir, ...
New
1player
A question I had when first learning contexts and Ecto was how to expand the default context API to support more flexible queries. Usuall...
New
voltone
The EEF’s Security WG has released the first public draft of the Secure Coding and Deployment Hardening Guidelines for BEAM languages. “...
New
dkuhlman
For those of you who might be interested in using ZeroMQ in Elixir, I converted the Erlang examples in the ZeroMQ “zguide” to Elixir. I ...
New
kokolegorille
Hello dear alchemists, There was this question some days ago here about the deployment to a VPS. As I was in the process of deploying t...
New
slouchpie
Warmest greetings, comrades. I recently started using :dns_cluster (GitHub - phoenixframework/dns_cluster: Simple DNS clustering for dis...
New
KoviRobi
Hi, I’ve written the following to debug function calls, not sure if it’s useful for anyone else, and if so should I put it somewhere? i...
New

Other popular topics Top

lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
TunkShif
This post is an instruction guide to help you setup your Neovim for Elixir development from scratch. It includes general information on h...
274 41539 114
New
mcarvalho
What is the difference between System.get_env and Application.get_env? For example, what are best practices to use one versus another.
New
Nvim
Anybody knows a comprehensive comparison of Django and Phoenix, thanks for the help. Where are they similar? Where do they differ the m...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
jononomo
I am trying to figure out how Mix knows whether the environment is test, dev, or prod – where is this set? Thanks.
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
alice
Hey, Just curious what are the main benefits of Elixir compared to Clojure? When is Elixir more useful than Clojure and vice versa? Th...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
sergio
Kind of like when jquery came out, it was super necessary. Existing drag and drop libraries have a bunch of baggage to support old browse...
New

Latest on Elixir Forum

We're in Beta

About us Mission Statement