Adapting MOD files for C++ with NEURON 9.0[.dev]

In older versions of NEURON, MOD files containing NMODL code were translated into C code before being compiled and executed by NEURON. Starting with NEURON 9.0[.dev], NMODL code is translated into C++ code instead.

In most cases, this does not present any issues, as simple C code is typically valid C++, and no changes are required. However, C and C++ are not the same language, and there are cases in which MOD files containing VERBATIM blocks need to be modified in order to build with NEURON 9.0[.dev].

Before you start, you should decide if you need your MOD files to be compatible simultaneously with NEURON 9.0[.dev] and older versions, or if you can safely stop supporting older versions. Supporting both is generally possible, but it may be more cumbersome than committing to using C++ features. Considering NEURON has maintained strong backward compatibility and its internal numerical methods haven’t changed with migration to C++, it is likely to be sufficient to adapt MOD files to C++ only and use NEURON 9.0[.dev]. If you do decide to preserve compatibility across versions, the preprocessor macros described in VERBATIM may prove useful.

Note

If you have a model that stopped compiling when you upgraded to or beyond NEURON 9.0[.dev], the first thing that you should check is whether the relevant MOD files have already been updated in ModelDB or in the GitHub repository of that model. You can check the repository name with the model accession number under https://github.com/ModelDBRepository. An updated version may already be available!

The following models were updated in ModelDB in preparation for NEURON 9.0[.dev] and may serve as useful references: 2487, 2730, 2733, 3658, 7399, 7400, 8284, 9889, 12631, 26997, 35358, 37819, 51781, 52034, 64229, 64296, 87585, 93321, 97868, 97874, 97917, 105507, 106891, 113732, 116094, 116830, 116838, 116862, 123815, 136095, 136310, 137845, 138379, 139421, 140881, 141505, 144538, 144549, 144586, 146949, 149000, 149739, 150240, 150245, 150551, 150556, 150691, 151126, 151282, 153280, 154732, 155568, 155601, 155602, 156780, 157157, 168874, 181967, 182129, 183300, 185355, 185858, 186768, 187604, 189154, 194897, 195615, 223031, 225080, 231427, 232097, 239177, 241165, 241240, 244262, 244848, 247968, 249463, 256388 and 259366.

Legacy patterns that are invalid C++

This section aims to list some patterns that the NEURON developers have found in pre-NEURON 9.0[.dev] models that need to be modified to be valid C++.

Implicit pointer type conversions

C++ has stricter rules governing pointer type conversions than C. For example

double* x = (void*)0;   // valid C, invalid C++
double* x = nullptr;    // valid C++, invalid C
double* x = (double*)0; // valid C and C++ (C-style casts discouraged in C++)

Similarly, in C one can pass a void* argument to a function that expects a double*. In C++ this is forbidden.

The same issue may manifest itself in code such as

double* x = malloc(7 * sizeof(double));          // valid C, invalid C++
double* x = new double[7];                       // valid C++, invalid C
double* x = (double*)malloc(7 * sizeof(double)); // valid C and C++ (C-style casts discouraged in C++)

If you choose to move from using C malloc and free to using C++ new and delete then remember that you cannot mix and match new with free and so on.

Note

Explicit memory management with new and delete is discouraged in C++ (R.11: Avoid calling new and delete explicitly). If you do not need to support older versions of NEURON, you may be able to use standard library containers such as std::vector<T>.

Local non-prototype function declarations

In C, the function declaration

void some_function();

is a non-prototype function declaration: it declares that some_function exists, but it does not specify the number of arguments that it takes, or their types. In C++, the same code declares that some_function takes zero arguments (the C equivalent of this is void some_function(void)).

If such a declaration occurs in a top-level VERBATIM block then it is likely to be harmless: it will add a zero-parameter overload of the function, which will never be called. It will, however, cause a problem if the declaration is included inside an NMODL construct

PROCEDURE procedure() {
VERBATIM
  void some_method_taking_an_int(); // problematic in C++
  some_method_taking_an_int(42);
ENDVERBATIM
}

because in this case the local declaration hides the real declaration, which is something like

void some_method_taking_an_int(int);

in a NEURON header that is included when the translated MOD file is compiled. In this case, the problematic local declaration can simply be removed.

Function declarations with incorrect types

In older MOD files and versions of NEURON, API methods were often accessed by declaring them in the MOD file and not by including a correct declaration from NEURON itself. In NEURON 8.2+, more declarations are implicitly included when MOD files are compiled. This can lead to problems if the declaration in the MOD file did not specify the correct argument and return types.

Rand* nrn_random_arg(int); // correct declaration from NEURON header, only in new NEURON
/* ... */
void* nrn_random_arg(int); // incorrect declaration in MOD file, clashes with previous line

The fix here is simply to remove the incorrect declaration from the MOD file.

If the argument types are incorrect, the situation is slightly more nuanced. This is because C++ supports overloading functions by argument type, but not by return type.

void sink(Thing*); // correct declaration from NEURON header, only in new NEURON
/* ... */
void sink(void*); // incorrect declaration in MOD file, adds an overload, not a compilation error
/* ... */
void* void_ptr;
sink(void_ptr);  // probably used to work, now causes a linker error
Thing* thing_ptr;
sink(thing_ptr); // works with both old and new NEURON

Here the incorrect declaration sink(void*) declares a second overload of the sink function, which is not defined anywhere. With NEURON 9.0[.dev] the sink(void_ptr) line will select the void* second overload, based on the argument type, and this will fail during linking because this overload is not defined (only sink(Thing*) has a definition).

In contrast, sink(thing_ptr) will select the correct overload in NEURON 9.0[.dev], and it also works in older NEURON versions because Thing* can be implicitly converted to void*.

The fix here is, again, to remove the incorrect declaration from the MOD file.

See also the section below, Deprecated overloads taking void*, for cases where NEURON does provide a (deprecated) definition of the void* overload.

K&R function definitions

C supports (until C23) a legacy (“K&R”) syntax for function declarations. This is not valid C++.

There is no advantage to the legacy syntax. If you have legacy definitions such as

void foo(a, b) int a, b; { /* ... */ }

then simply replace them with

void foo(int a, int b) { /* ... */ }

which is valid in both C and C++.

Legacy patterns that are considered deprecated

As noted above (Local non-prototype function declarations), declarations such as

VERBATIM
extern void vector_resize();
ENDVERBATIM

at the global scope in MOD files declare C++ function overloads that take no parameters. If such declarations appear at the global scope then they do not hide the correct declarations, so this can be harmless, but it is not necessary. In NEURON 9.0[.dev] the full declarations of the NEURON API methods that can be used in MOD files are implicitly included via the mech_api.h header, so this explicit declaration of a zero-parameter overload is not needed and can safely be removed.

Deprecated overloads taking void*

As noted above (Function declarations with incorrect types), NEURON 9.0[.dev] provides extra overloads for some API methods that aim to smooth the transition from C to C++. These overloads are provided for widely used methods, and where overloading on return type would not be required.

An example is the vector_capacity function, which in NEURON 9.0[.dev] has two overloads

int vector_capacity(IvocVect*);
[[deprecated("non-void* overloads are preferred")]] int vector_capacity(void*);

The second one simply casts its argument to IvocVect* and calls the first one. The [[deprecated]] attribute means that MOD files that use the second overload emit compilation warnings when they are compiled using nrnivmodl.

If your MOD files produce these deprecation warnings, make sure that the relevant method (vector_capacity in this example) is being called with an argument of the correct type (IvocVect*), and not a type that is implicitly converted to void*.

Legacy random number generators and API

Various changes have also been done in the API of NEURON functions related to random number generators.

First, in NEURON 9.0[.dev] parameters passed to the functions need to be of the correct type as it was already mentioned in Function declarations with incorrect types. The most usual consequence of that is that NEURON random API functions that were taking as an argument a void* now need to called with a Rand*. An example of the changes needed to fix this issue is given in 182129.

Another related change is with scop_rand() function that is usually used for defining a URAND FUNCTION in mod files to return a number based on a uniform distribution from 0 to 1. This function now takes no argument anymore. An example of this change can also be found in 182129.

Finally, the preferred random number generator is Random123. You can find more information about that in Random.Random123() and Randomness in NEURON models. An example of the usage of Random123 can be seen in netstim.mod and its corresponding test.` Another important aspect of Random123 is that it’s supported in CoreNEURON as well. For more information about this see Random Number Generators: Random123 vs MCellRan4.