\(\renewcommand{\AA}{\text{Å}}\)

4.8.8. Writing a new command style

Command styles allow to do system manipulations or interfaces to the operating system.

In the text below, we will discuss the implementation of one example. As shown on the page for writing or extending command styles, in order to implement a new command style, a new class must be written that is either directly or indirectly derived from the Command class. There is just one method that must be implemented: Command::command(). In addition, a custom constructor is needed to get access to the members of the LAMMPS class like the Error class to print out error messages. The Command::command() method processes the arguments passed to the command in the input and executes it. Any other methods would be for the convenience of implementation of the new command.

In general, new command styles should be added to the EXTRA-COMMAND package. If you feel that your contribution should be added to a different package, please consult with the LAMMPS developers first. The contributed code needs to support the traditional GNU make build process and the CMake build process.


4.8.9. Case 1: Implementing the geturl command

In this section, we will describe the procedure of adding a simple command style to LAMMPS: the geturl command that allows to download files directly without having to rely on an external program like “wget” or “curl”. The complete implementation can be found in the files src/EXTRA-COMMAND/geturl.cpp and src/EXTRA-COMMAND/geturl.h of the LAMMPS source code.

Interfacing the libcurl library

Rather than implementing the various protocols for downloading files, we rely on an external library: libcurl library. This requires that the library and its headers are installed. For the traditional GNU make build system, this simply requires edits to the machine makefile to add compilation flags like for other libraries. For the CMake based build system, we need to add some lines to the file cmake/Modules/Packages/EXTRA-COMMAND.cmake:

find_package(CURL QUIET COMPONENTS HTTP HTTPS)
option(WITH_CURL "Enable libcurl support" ${CURL_FOUND})
if(WITH_CURL)
  find_package(CURL REQUIRED COMPONENTS HTTP HTTPS)
  target_compile_definitions(lammps PRIVATE -DLAMMPS_CURL)
  target_link_libraries(lammps PRIVATE CURL::libcurl)
endif()

The first find_package() command uses a built-in CMake module to find an existing libcurl installation with development headers and support for using the HTTP and HTTPS protocols. The “QUIET” flag ensures that there is no screen output and no error if the search fails. The status of the search is recorded in the “${CURL_FOUND}” variable. That variable sets the default of the WITH_CURL option, which toggles whether support for libcurl is included or not.

The second find_package() uses the “REQUIRED” flag to produce an error if the WITH_CURL option was set to True, but no suitable libcurl implementation with development support was found. This construct is used so that the CMake script code inside the if(WITH_CURL) and endif() block can be expanded later to download and compile libcurl as part of the LAMMPS build process, if it is not found locally. The target_compile_definitions() function added the define -DLAMMPS_CURL to the compilation flags when compiling objects for the LAMMPS library. This allows to always compile the geturl command, but use pre-processing to compile in the interface to libcurl only when it is present and usable and otherwise stop with an error message about the unavailability of libcurl to execute the functionality of the command.

Header file

The first segment of any LAMMPS source should be the copyright and license statement. Note the marker in the first line to indicate to editors like emacs that this file is a C++ source, even though the .h extension suggests a C source (this is a convention inherited from the very beginning of the C++ version of LAMMPS).

/* -*- c++ -*- ----------------------------------------------------------
   LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator
   https://www.lammps.org/, Sandia National Laboratories
   LAMMPS development team: developers@lammps.org

   Copyright (2003) Sandia Corporation.  Under the terms of Contract
   DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains
   certain rights in this software.  This software is distributed under
   the GNU General Public License.

   See the README file in the top-level LAMMPS directory.
------------------------------------------------------------------------- */

Every command style must be registered in LAMMPS by including the following lines of code in the second part of the header after the copyright message and before the include guards for the class definition:

#ifdef COMMAND_CLASS
// clang-format off
CommandStyle(geturl,GetURL);
// clang-format on
#else

This block between #ifdef COMMAND_CLASS and #else will be included by the Input class in input.cpp to build a map of “factory functions” that will create an instance of a Command class and call its command() method. The map connects the name of the command geturl with the name of the class GetURL. During compilation, LAMMPS constructs a file style_command.h that contains #include statements for all “installed” command styles. Before including style_command.h into input.cpp, the COMMAND_CLASS define is set and the CommandStyle(name,class) macro defined. The code of the macro adds the installed command styles to the “factory map” which enables the Input to execute the command.

The list of header files to include in style_command.h is automatically updated by the build system if there are new files, so the presence of the new header file in the src/EXTRA-COMMAND folder and the enabling of the EXTRA-COMMAND package will trigger LAMMPS to include the new command style when it is (re-)compiled. The “// clang-format” format comments are needed so that running clang-format on the file will not insert unwanted blanks which would break the CommandStyle macro.

The third part of the header file is the actual class definition of the GetURL class. This has the custom constructor and the command() method implemented by this command style. For the constructor there is nothing to do but to pass the lmp pointer to the base class. Since the command() method is labeled “virtual” in the base class, it must be given the “override” property.

#ifndef LMP_GETURL_H
#define LMP_GETURL_H

#include "command.h"

namespace LAMMPS_NS {

class GetURL : public Command {
 public:
  GetURL(class LAMMPS *lmp) : Command(lmp) {};
  void command(int, char **) override;
};
}    // namespace LAMMPS_NS
#endif
#endif

The “override” property helps to detect unexpected mismatches because compilation will stop with an error in case the signature of a function is changed in the base class without also changing it in all derived classes.

Implementation file

We move on to the implementation of the GetURL class in the geturl.cpp file. This file also starts with a LAMMPS copyright and license header. Below that notice is typically the space where comments may be added with additional information about this specific file, the author(s), affiliation(s), and email address(es). This way the contributing author(s) can be easily contacted, when there are questions about the implementation later. Since the file(s) may be around for a long time, it is beneficial to use some kind of “permanent” email address, if possible.

/* ----------------------------------------------------------------------
   LAMMPS - Large-scale Atomic/Molecular Massively Parallel Simulator
   https://www.lammps.org/, Sandia National Laboratories
   LAMMPS development team: developers@lammps.org

   Copyright (2003) Sandia Corporation.  Under the terms of Contract
   DE-AC04-94AL85000 with Sandia Corporation, the U.S. Government retains
   certain rights in this software.  This software is distributed under
   the GNU General Public License.

   See the README file in the top-level LAMMPS directory.
------------------------------------------------------------------------- */

/* ----------------------------------------------------------------------
   Contributing authors:  Axel Kohlmeyer (Temple U),
------------------------------------------------------------------------- */

#include "geturl.h"

#include "comm.h"
#include "error.h"

#if defined(LAMMPS_CURL)
#include <curl/curl.h>
#endif

using namespace LAMMPS_NS;

The second section of the implementation file has various include statements. The include file for the class header has to come first, then a couple of LAMMPS classes (sorted alphabetically) followed by the header for the libcurl interface. This is wrapped into an #ifdef block so that LAMMPS will compile this file without error when the libcurl header is not available and thus the define not set. The final statement of this segment imports the LAMMPS_NS:: namespace globally for this file. This way, all LAMMPS specific functions and classes do not have to be prefixed with LAMMPS_NS::.

The command() function (required)

Since the required custom constructor is trivial and implemented in the header, there is only one function that must be implemented for a command style and that is the command() function.

void GetURL::command(int narg, char **arg)
{
#if !defined(LAMMPS_CURL)
  error->all(FLERR, "LAMMPS has not been compiled with libcurl support");
#else
  if (narg < 1) utils::missing_cmd_args(FLERR, "geturl", error);
  int verify = 1;
  int overwrite = 1;
  int verbose = 0;

This first part also has the #ifdef block depending on the LAMMPS_CURL define. This way the command will simply print an error, if libcurl is not available but will not fail to compile. Furthermore, it sets the defaults for the following optional arguments.

// process arguments

std::string url = arg[0];

// sanity check

if ((url.find(':') == std::string::npos) || (url.find('/') == std::string::npos))
  error->all(FLERR, "URL '{}' is not a supported URL", url);

std::string output = url.substr(url.find_last_of('/') + 1);
if (output.empty()) error->all(FLERR, "URL '{}' must end in a file string", url);

This block stores the positional, i.e. non-optional argument of the URL to be downloaded and adds a couple of sanity checks on the string to make sure it is a valid URL. Also it derives the default name of the output file from the URL.

int iarg = 1;
while (iarg < narg) {
  if (strcmp(arg[iarg], "output") == 0) {
    if (iarg + 2 > narg) utils::missing_cmd_args(FLERR, "geturl output", error);
    output = arg[iarg + 1];
    ++iarg;
  } else if (strcmp(arg[iarg], "overwrite") == 0) {
    if (iarg + 2 > narg) utils::missing_cmd_args(FLERR, "geturl overwrite", error);
    overwrite = utils::logical(FLERR, arg[iarg + 1], false, lmp);
    ++iarg;
  } else if (strcmp(arg[iarg], "verify") == 0) {
    if (iarg + 2 > narg) utils::missing_cmd_args(FLERR, "geturl verify", error);
    verify = utils::logical(FLERR, arg[iarg + 1], false, lmp);
    ++iarg;
  } else if (strcmp(arg[iarg], "verbose") == 0) {
    if (iarg + 2 > narg) utils::missing_cmd_args(FLERR, "geturl verbose", error);
    verbose = utils::logical(FLERR, arg[iarg + 1], false, lmp);
    ++iarg;
  } else {
    error->all(FLERR, "Unknown geturl keyword: {}", arg[iarg]);
  }
  ++iarg;
}

This block parses the optional arguments following the URL and stops with an error if there are arguments missing or an unknown argument is encountered.

// only download files from rank 0

if (comm->me != 0) return;

if (!overwrite && platform::file_is_readable(output)) return;

// open output file for writing

FILE *out = fopen(output.c_str(), "wb");
if (!out)
  error->all(FLERR, "Cannot open output file {} for writing: {}", output, utils::getsyserror());

Here all MPI ranks other than 0 will return, so that the URL download will only happen from a single MPI rank. For that rank the output file is opened for writing using the C library function fopen().

// initialize curl and perform download

CURL *curl;
curl_global_init(CURL_GLOBAL_DEFAULT);
curl = curl_easy_init();
if (curl) {
  (void) curl_easy_setopt(curl, CURLOPT_URL, url.c_str());
  (void) curl_easy_setopt(curl, CURLOPT_WRITEDATA, (void *) out);
  (void) curl_easy_setopt(curl, CURLOPT_FILETIME, 1L);
  (void) curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1L);
  if (verbose && screen) {
    (void) curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L);
    (void) curl_easy_setopt(curl, CURLOPT_STDERR, (void *) screen);
  }
  if (!verify) {
    (void) curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
    (void) curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
  }
  auto res = curl_easy_perform(curl);
  if (res != CURLE_OK) {
    long response = 0L;
    curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response);
    error->one(FLERR, "Download of {} failed with: {} {}", output, curl_easy_strerror(res),
               response);
  }
  curl_easy_cleanup(curl);

This block now implements the actual URL download with the selected options via the “easy” interface of libcurl. For the details of what these function calls do, please have a look at the *libcurl documentation.

  }
  curl_global_cleanup();
  fclose(out);
#endif
}

Finally, the previously opened file is closed and the command is complete.