Using Emacs yasnippet against repetitive boilerplate code

Yasnippet is an emacs package for snippet expansion into code according to predefined rules. In this post I show how to use it for doing away with common boilerplate code in C.

This blogpost will target the C language but all of its contents can easily be transferred to any other verbose enough language where repetition some times may arise.

I love working with C code, for the simplicity and clarity it gives if written well when compared to the higher-level alternative languages like C++. The other side of the coin for this is that C requires you to write a lot of boilerplate code. You can either go ahead, bite the bullet and write it every time or try to abstract it away in preprocessor macros.

The macros way can go really far if you combine it with preprocessor magic like counting the number of arguments passed to a variadic macro. I have used the macros approach personally in a C project I am working on. It was okay at first but then it became so ugly to look at and messed up with code jumping in any IDE (including Emacs) that I decided to abandon the approach.

Introducing yasnippet

So what is the alternative? I have been experimenting a bit with yasnippet lately and I find it to be a very useful tool for exactly the problem I described above.

Yasnippet works in a quite simple but powerfull manner. It has the definition of a code snippet which is a templated piece of code in a specific language. You can define a keyword after which you can press a particular key, the TAB key by default, and then your keyword will expand into the code snippet following the template rules. It comes with some predefined snippets for various language modes but also allows you to define your own.

You can have yasnippet enabled globally. But if you are like me and only need it in some specific modes you can have something like this in your emacs configuration:

;;; init-yasnippet --- my yasnippet configuration
;;; Commentary:
;;; Code snippets expansion
;;; https://capitaomorte.github.io/yasnippet/
;;;
;;; Code:
(require 'yasnippet)
(yas-reload-all)

(add-hook 'c-mode-common-hook #'yas-minor-mode)
(provide 'init-yasnippet)
;;; init-yasnippet.el ends here

The templated language for yasnippet is really simple. You can describe how you would like the code to look like and insert variables in the form of $n where n is a number that refers to the number of tab completions you want to do when filling in the snippet. At the expansion of the snippet you will be prompted for each of these variables. A variable can also have a default value which is specified like this: ${1:object}.

Using yasnippet for boilerplate code

Yasnippet has some snippets already predefined for C code which you can find under c-mode in the yasnippet, snippets directory but I personally find them lacking. As an example here is a snippet to expand pr into printf taken from the predefined snippets

# -*- mode: snippet -*-
# name: printf
# key: pr
# --
printf("${1:format string}"${2: ,a0,a1});

A simple example for C structs

But since we can create our own snippets let’s get something useful! For those of you who have programmed long in C you must already be quite familiar with the 4 much needed functions to:

  • Allocate a new object of a structure.
  • Initialize a new object of a structure.
  • Destroy an object of a structure.
  • Deinitialize an object of a structure.

Most of the times the code for the above is very repetitive and is the perfect candidate for a snippet. Below you can see a yasnippet that accomplishes exactly that.

# name : C struct Creation and destruction functions
# key : crtdstr
# --

bool ${1:prefix}_${2:name}_init(struct $1_$2 *${3:obj})
{
    return true;
}

struct $1_$2 *$1_$2_create()
{
    struct $1_$2 *ret;
    RF_MALLOC(ret, sizeof(*ret), return NULL);
    if (!$1_$2_init(ret)) {
        free(ret);
        ret = NULL;
    }
    return ret;
}

void $1_$2_deinit(struct $1_$2 *$3)
{

}

void $1_$2_destroy(struct $1_$2 *$3)
{
    $1_$2_deinit($3);
    free($3);
}

In order to use it, just add it under one of the directoris defined in yas-snippet-dirs under a subdirectory called c-mode.

The above snippet has 3 template variables. One for the prefix of the object, one for its name and one for the name of the object variable. These will be filled in with 3 consecutive TAB completions.

The prefix is usually used as a poor man’s way to namespace things in C, so it’s usually the name of the module or library.

An example of the above snippet in action can be seen below

yasnippet1.gif

The demo starts with the keyword crtdstr we defined in the snippet. Then the steps are:

  1. <Tab> -> Expand the snippet and prompt for first variable
  2. Input value for first variable, press <Tab> to continue
  3. Input value for second variable, press <Tab> to continue
  4. Input value for third variable, press <Tab> to finish the snippet

And here is the created C code. RF_MALLOC() is a macro I am using for malloc(), plus error reporting.

bool rf_string_init(struct rf_string *str)
{
    return true;
}

struct rf_string *rf_string_create()
{
    struct rf_string *ret;
    RF_MALLOC(ret, sizeof(*ret), return NULL);
    if (!rf_string_init(ret)) {
        free(ret);
        ret = NULL;
    }
    return ret;
}

void rf_string_deinit(struct rf_string *str)
{

}

void rf_string_destroy(struct rf_string *str)
{
    rf_string_deinit(str);
    free(str);
}

A more complete example for C code

We can also see a more complete version of the above example where we will do the same thing as above but form a full C file and infer the name of the structure from the filename.

# -*- mode: snippet -*-
# name : C full definition of a file with include guards and functions
# key : cfullfile
# --
#ifndef ${1:$(upcase yas-text)}_${2:`(upcase (file-name-nondirectory (file-name-sans-extension (buffer-file-name))))`_H}
#define ${1:$(upcase yas-text)}_$2

struct ${1:prefix}_${3:`(downcase (file-name-nondirectory (file-name-sans-extension (buffer-file-name))))`} {
$0
};

bool $1_$3_init(struct $1_$3 *${4:obj})
{
    return true;
}

struct $1_$3 *$1_$3_create()
{
    struct $1_$3 *ret;
    RF_MALLOC(ret, sizeof(*ret), return NULL);
    if (!$1_$3_init(ret)) {
        free(ret);
        ret = NULL;
    }
    return ret;
}

void $1_$3_deinit(struct $1_$3 *$4)
{

}

void $1_$3_destroy(struct $1_$3 *$4)
{
    $1_$3_deinit($4);
    free($4);
}
#endif /* ${1:$(upcase yas-text)}_$2 */

Add this to a new file under the c-mode snippets. Below you can see an example of the snippet expansion in action ran on a file named tree.c.

yasnippet2.gif

From the filename we infer all the needed information and we simply provide the rf prefix as required. After that we just <Tab> over the other variables confirming their values. We could also change them if we wanted. The resulting C code can be seen below

#ifndef RF_TREE_H
#define RF_TREE_H

struct rf_tree {
    // here fill in the struct
};

bool rf_tree_init(struct rf_tree *t)
{
    return true;
}

struct rf_tree *rf_tree_create()
{
    struct rf_tree *ret;
    RF_MALLOC(ret, sizeof(*ret), return NULL);
    if (!rf_tree_init(ret)) {
        free(ret);
        ret = NULL;
    }
    return ret;
}

void rf_tree_deinit(struct rf_tree *t)
{

}

void rf_tree_destroy(struct rf_tree *t)
{
    rf_tree_deinit(t);
    free(t);
}
#endif /* RF_TREE_H */

How is that achieved? If you take a careful look at the snippet you can see that we have used embedded elisp code in it. For example in order to get the value that should go to the #ifndef guards we used the following.

${1:$(upcase yas-text)}_${2:`(upcase (file-name-nondirectory (file-name-sans-extension (buffer-file-name))))`_H}

Remember that variables are numbered for a reason. This may be in the beginning of the snippet but assumes that $1 is already set and uppercases it. Following that it gets the filename and uppercases that. This, as usual in C code, forms the name of the #ifndef guard.

A bit further down we see the following:

struct ${1:prefix}_${3:`(downcase (file-name-nondirectory (file-name-sans-extension (buffer-file-name))))`} {
$0
};

Here is where $1 is first prompted for insertion and where we acquire the prefix. That in combination with similar code to the #ifndef guards form the name of the structure inferred from the file name.

A final thing to note is the special variable $0 in the body of the structure. This is where the cursor of the user will end up after the snippet expansion is finished.

Conclusion

This was just a simple example of using yasnippet to automate and simplify repetitive boilerplate code. If you put enough time into it you can come up with quite more powerful and empowering snippets. To do that important references would be:

Malinka, an Emacs C/C++ project configuration package

Introducing malinka, a C/C++ project configuration package for Emacs. Malinka can be used to configure projects in C or C++ and provide correnc input to other packages like rtags and flycheck so that a modern code navigation and editing experience can be achieved.

Working with C/C++ code in Emacs can sometimes become a really frustrating undertaking. Using ctags, gtags or Cedet does not always provide the best results that one would desire from a modern editor when navigating C/C++ code.

In an earlier post I had written of a way to force Semantic to parse all files under a particular source tree so that it could know about all the tags of a project. While it worked most of the times, it is a crude way to deal with the problem. Not to mention it’s terrible lisp code. Since then I dealt a little bit more with elisp and emacs configuration and came to the conclusion that there are better ways of dealing with the problem.

Introducing Malinka

Enter Malinka, a C/C++ project configuration package for Emacs. It’s a kind of a meta-package combining the power of a few other packages to create a modern Emacs development experience for C/C++ programmers.

The main functionality that malinka provides is a way to configure C/C++ projects and through that configuration empower rtags and optionally other packages like flycheck with correct input at the correct time so that a modern C/C++ code editing and especially navigation experience is achieved.

Prerequisites

In order to be able to use Malinka you need to have a few basic things setup in your .emacs.d. As long as you have dealt with the first prerequisite you don’t have to worry about the rest. It’s just a matter of M-x el-get-install RET malinka RET if you are using el-get or M-x package-instal RET malinka RET if you are using melpa. Those commands would pull and build all other bullet points but I am mentioning them here for documentation purposes

  • el-get or melpa: el-get is a modern way to manage your emacs packages. The easiest way to install malinka is by using el-get to pull and build all dependencies. Alternatively you can use package-instal having the melpa package repositories configured in your package archives.
  • rtags: rtags is a dependency that el-get would get and build for you manually. It’s a package based on a server and client model. The rtags daemon is called rdm and uses clang to parse and maintain all tags of a malinka project and inotify to reparse any files that have been edited. The rtags client is called rc and is the method by which we communicate with the daemon. If desired you can start the daemon manually by invoking something like
    rdm > /dev/null 2>&1 &
    

    But there is no need to worry about that since as long as the rtags package is installed malinka will assert that rdm is running and if not it will attempt to start it.

  • projectile: projectile is a generic project creation and management library for Emacs. It contains a lot of very useful functionality to detect and manage projects mainly using VCS. In order to not reinvent the wheel some of projectile’s functionality is used by malinka.
  • flycheck: (optional) Flycheck is an optional dependency, but one that can integrate very nicely. Using flycheck and clang’s syntax checker malinka can provide correct input data to flycheck depending on the currently visited buffer. As a result the syntax checker can show all warnings being aware of the different compilation parameters of each buffer.

Setup

After you have installed malinka using el-get or any other method, you need to set it up so that it becomes aware of your C/C++ projects. The setup step is basically about letting clang know the compile commands of your project.

The suggested method from rtags itself is to symlink the rtags wrapper script to your compiler. It is indeed the most hassle-free method but with malinka we are going to be trying a different approach. The malinka approach is far from perfect and as can be seen in the future work section there are some plans to improve it.

Simple project setup

The way to let it know about a particular project is by using (malinka-define-project). As a very minimal example picture the setup of Refu C library, an old project of mine.

(require 'malinka)

(malinka-define-project
 :name "Refulib"
 :cpp-defines '("REFU_COMPILING" "REFU_LINUX_VERSION" "REFU_TEST"
                "RF_MODULE_DF_XML" "RF_MODULE_DS_ARRAY" "RF_MODULE_DS_BARRAY"
                "RF_MODULE_DS_LIST" "RF_MODULE_IO" "RF_MODULE_IO_TEXTFILE"
                "RF_MODULE_REGEX" "RF_MODULE_STRINGS" "RF_MODULE_SYSTEM"
                "RF_MODULE_THREAD" "RF_MODULE_THREADX" "RF_MODULE_TIME_DATE"
                "RF_MODULE_TIME_TIMER" "RF_OPTION_DEFAULT_ARGUMENTS"
                "RF_OPTION_FGETS_READ_BYTESN=512" "RF_OPTION_FILE_ADDBOM"
                "RF_OPTION_LIST_CAPACITY_M=2"
                "RF_OPTION_LOCALSTACKMEMORY_SIZE=1048576"
                "RF_OPTION_REGEX_NODES_ALLOCMULTIPLIER=4"
                "RF_OPTION_REGEX_STACKSIZE=250"
                "RF_OPTION_STRINGX_CAPACITY_M=2"
                "RF_OPTION_THREADX_MSGQUEUE=10" "RF_OPTION_VERBOSE_ERRORS"
                "_FILE_OFFSET_BITS=64" "_GNU_SOURCE" "_LARGEFILE64_SOURCE"
                "RF_MODULE_LIST_EXTRA" "RF_MODULE_LIST")
:compiler-flags '("-Wall" "-g")
:include-dirs '("/home/lefteris/w/Refulib/include")
:root-directory "~/w/Refulib")

(malinka-define-project) can accept more arguments than seen in this example so we should look at all of them one by one. (For a full and up to date list always do check the source code too!):

  • name: The name of the project. Should be the same name as the root directory of the project.
  • cpp-defines: The macros definitions to be passed to the C preprocessor.
  • include-dirs: The include directories of the project where the compiler should search for headers.
  • compiler-flags: Other flags to pass to the compiler.
  • root-directory: The full path to the root directory of the project. If not specified then projectile is used to try and find the root judging by the current’s buffer location and the malinka project name.
  • compiler-executable: The full path to the compiler executable. Defaults to /usr/bin/gcc.

The difficulty in this approach is that we need to manually copy all of the compiler options in the malinka project definition. In the future work section one can see suggested improvements to this.

Setup of multiple versions of a project

Some teams have their workflow organized in a way so that different versions of the code is kept under different directories. The reason for such a setup is only if the product you are working on is complex and comprised of multiple repositories. Then it makes sense to have 2 different versions of all repos checked out so that one can work on both at the same time easily. For example:

+
|
+ version 1.20 +
|              |
|              project_foo +
|              |           |
|              |           sources
|              |           ...
|              lib_foo     +
|              |           |
|              |           sources
|              ...      
|
+ version 1.60 +
|              |
|              project_foo +
|              |           |
|              |           sources
|              |           ...
|              lib_foo     +
|              |           |
|              |           sources
|              ...      
|

In such a case the setup that you would keep for either project_foo or lib_foo would not specify a root directory but utilize the same-name-check parameter of (malinka-define-project)

(malinka-define-project
 :name "project_foo"
 :cpp-defines '("SOMETHING_IS_DEFINEd")
 :compiler-flags '("-Wall" "-g")
 :include-dirs '("/path/to/include")
 :same-name-check nil)

Having the same-name-check parameter set to nil along with the absence of a root-directory allows for such a multiple versions of the same project setup to work. We will see how in the next section.

Usage

The first time you want to use malinka in a project you have to configure that project for your system. The way to achieve this is by calling M-x malinka-project-configure from any buffer belonging to the project you need to configure. Once this is done a json file containing the clang compilation database for this project will be created. The name of the file is compile_commands.json. I suggest you add it to the ignores of your version control system.

Note that you do not really need to be in a buffer belonging to the project in order to call M-x malinka-project-configure. It will prompt you with a choice of project names that it knows about. If malinka notices that the current buffer belongs to a known project it will be provided as the default choice. So it’s generally good practise to be in a buffer belonging to the project you want to configure.

In the case of a project with multiple versions as shown here, there is an extra selection that the user is prompted for when invoking M-x malinka-project-configure after having selected the project name. Malinka will prompt you for the root directory of the project so that you can choose which version/checkout you need configured.

After that is done and after rtags have indexed all of the project (it can take some time for large codebases) you can use all of the rtags commands in order to jump around and navigate in the C/C++ source code of your project.

Using rtags with malinka

For all the possible commands and in order to be up to date I suggest directly checking the rtags documentation and their source code to see what is possible. As a simple example and to give you a taste of the power of rtags take a look at the default keybindings of their API.

(defun rtags-enable-standard-keybindings (&optional map prefix)
  (interactive)
  (unless map
    (setq map c-mode-base-map))
  (unless prefix
    (setq prefix "\C-xr"))
  (ignore-errors
    (define-key map (concat prefix ".") (function rtags-find-symbol-at-point))
    (define-key map (concat prefix ",") (function rtags-find-references-at-point))
    (define-key map (concat prefix "v") (function rtags-find-virtuals-at-point))
    (define-key map (concat prefix "V") (function rtags-print-enum-value-at-point))
    (define-key map (concat prefix "/") (function rtags-find-all-references-at-point))
    (define-key map (concat prefix "Y") (function rtags-cycle-overlays-on-screen))
    (define-key map (concat prefix ">") (function rtags-find-symbol))
    (define-key map (concat prefix "<") (function rtags-find-references))
    (define-key map (concat prefix "[") (function rtags-location-stack-back))
    (define-key map (concat prefix "]") (function rtags-location-stack-forward))
    (define-key map (concat prefix "D") (function rtags-diagnostics))
    (define-key map (concat prefix "G") (function rtags-guess-function-at-point))
    (define-key map (concat prefix "p") (function rtags-set-current-project))
    (define-key map (concat prefix "P") (function rtags-print-dependencies))
    (define-key map (concat prefix "e") (function rtags-reparse-file))
    (define-key map (concat prefix "E") (function rtags-preprocess-file))
    (define-key map (concat prefix "R") (function rtags-rename-symbol))
    (define-key map (concat prefix "U") (function rtags-print-cursorinfo))
    (define-key map (concat prefix "O") (function rtags-goto-offset))
    (define-key map (concat prefix ";") (function rtags-find-file))
    (define-key map (concat prefix "F") (function rtags-fixit))
    (define-key map (concat prefix "x") (function rtags-fix-fixit-at-point))
    (define-key map (concat prefix "B") (function rtags-show-rtags-buffer))
    (define-key map (concat prefix "I") (function rtags-imenu))
    (define-key map (concat prefix "T") (function rtags-taglist))))

I will let it as an exercise to the user to experiment with most of those. But the main strength of malinka is that it empowers rtags with the correct input data so that (rtags-find-symbol-at-point) and (rtags-find-references-at-point) work properly.

Go to any source file of your project and no matter how complicated the definition of a tag is, no matter how far away in the include chain it lies, your cursor will always correctly jump to it.

After that if you want to jump back to the previous tags and follow the tag jumping backwards you would call (rtags-location-stack-back). And vice versa if you need to go forward again you would call (rtags-location-stack-forward).

I suggest binding all those to convenient keys since you will be using them quite often 🙂

Malinka API

At the moment these are the functions one can use to interact with a project that has been registered with malinka:

  • M-x malinka-project-configure: As seen here this is the command to configure a project that has been defined with (malinka-define-project). Configuring means creating the clang compilation database and registering it with the rtags daemon.
  • M-x malinka-project-add-file: Allows adding a file to the project after configuration and indexes it with the rtags daemon.
  • M-x malinka-project-add-include-dir: Allows adding an include directory to the project’s compile commands and updates the rtags daemon.
  • M-x malinka-project-add-cpp-define: Allows adding a macro definition to the project’s compile commands and updates the rtags daemon.

Future Work / Conclusions

Malinka is a first approach of mine to create an Emacs package whose sole purpose is to manage C/C++ projects using the most modern related packages available to Emacs as of this moment. There is definitely room for improvement.

The main improvement that can be implemented is some form of Makefile or build command parsing so that cpp-defines, include directories and generally all compiler parameters are automatically acquired by malinka and passed directly to rtags. With such a feature, any compiler parameters given at (malinka-define-project) would simply be appended to what has already been parsed.

Finally I am no emacs lisp expert and I am always looking to learn. As a result any contributions to malinka are more than welcome, be it in the form of issue-reporting or pull requests/patches.

That’s all! Until next time.