Skip to main content
C modules are well-suited for flight-heritage code and for modules that must also run on embedded targets. Because C is not object-oriented, Basilisk reconstructs the class concept with a configuration structure and free functions. The cModuleTemplate folder in src/moduleTemplates/cModuleTemplate/ is the canonical starting point for any new C module.
This documentation accelerates the process but does not replace reading the Basilisk source code. Study how existing modules are written before writing your own.

Module file structure

Every C module lives in its own directory and contains the following files:
FilePurpose
myModule.hConfig struct, function prototypes, include guards
myModule.cSelfInit, Reset, and Update implementations
myModule.iSWIG interface — exposes the module to Python
myModule.rstModule RST documentation page
_UnitTest/test_myModule.pypytest unit test
Build the module outside the primary Basilisk source tree unless you intend to contribute the module back upstream. This keeps your Basilisk installation clean and upgradeable.

Defining a message payload

Before writing the module itself, define any new message types. Message payloads live in src/architecture/msgPayloadDefC/ and follow a strict naming convention.
// src/architecture/msgPayloadDefC/SomeMsgPayload.h
#ifndef SOME_MSG_H
#define SOME_MSG_H

/*! @brief Brief description of what this message contains */
typedef struct {
    int variable1;           //!< [units] variable description
    double variable2[3];     //!< [units] variable description
} SomeMsgPayload;

#endif
The file name must be upper camel case and end with MsgPayload.h. After creating the file, re-run python3 conanfile.py to auto-generate the C message wrapper code in dist3/autoSource/cMsgCInterface/.
If you rename or delete a *Payload.h file you must do a clean build (python3 conanfile.py --clean or delete dist3/) to avoid stale compiled message objects being imported from Python.

Writing the header file

The header file defines the module configuration structure and declares the three required functions.
// cModuleTemplate.h  (from src/moduleTemplates/cModuleTemplate/)
#ifndef _FSW_MODULE_TEMPLATE_H_
#define _FSW_MODULE_TEMPLATE_H_

#include <stdint.h>
#include "architecture/utilities/bskLogging.h"
#include "cMsgCInterface/CModuleTemplateMsg_C.h"

/*! @brief Top level structure for the sub-module routines. */
typedef struct {
    /* declare module private variables */
    double dummy;                          //!< [units] sample module variable declaration
    double dumVector[3];                   //!< [units] sample vector variable

    /* declare module IO interfaces */
    CModuleTemplateMsg_C dataOutMsg;       //!< sample output message
    CModuleTemplateMsg_C dataInMsg;        //!< sample input message

    double inputVector[3];                 //!< [units] vector description
    BSKLogger *bskLogger;                  //!< BSK Logging
} cModuleTemplateConfig;

#ifdef __cplusplus
extern "C" {
#endif

    void SelfInit_cModuleTemplate(cModuleTemplateConfig *configData, int64_t moduleID);
    void Update_cModuleTemplate(cModuleTemplateConfig *configData, uint64_t callTime, int64_t moduleID);
    void Reset_cModuleTemplate(cModuleTemplateConfig *configData, uint64_t callTime, int64_t moduleID);

#ifdef __cplusplus
}
#endif

#endif
Key points:
  • The config struct name ends with ...Config.
  • Input and output messages are both declared as SomeMsg_C — the same C wrapper type can act as either.
  • All config struct members are public; C modules have no concept of private variables.
  • The extern "C" block ensures the C functions link correctly from C++ translation units.
  • The #include path for message wrapper types is cMsgCInterface/SomeMsg_C.h. Including the _C.h file automatically pulls in the corresponding SomeMsgPayload.h.

Implementing the .c file

SelfInit

SelfInit initializes each output message object so that it writes to its own internal buffer. This is the C equivalent of a C++ constructor.
/*!
    This method initializes the output messages for this module.
    @param configData The configuration data associated with this module
    @param moduleID The module identifier
 */
void SelfInit_cModuleTemplate(cModuleTemplateConfig *configData, int64_t moduleID)
{
    CModuleTemplateMsg_C_init(&configData->dataOutMsg);
}
Only call ..._C_init() in SelfInit. Do not set default variable values here — the configData structure is zero-initialized by SWIG when the Python object is created, so any defaults set here will be overwritten.

Reset

Reset restores time-varying state variables to their default values and verifies that required input messages are connected.
/*! This method performs a complete reset of the module.  Local module variables that retain
 time varying states between function calls are reset to their default values.

 @param configData The configuration data associated with the module
 @param callTime [ns] time the method is called
 @param moduleID The module identifier
*/
void Reset_cModuleTemplate(cModuleTemplateConfig *configData, uint64_t callTime, int64_t moduleID)
{
    /*! reset any required variables */
    configData->dummy = 0.0;
    char info[MAX_LOGGING_LENGTH];
    sprintf(info, "Variable dummy set to %f in reset.", configData->dummy);
    _bskLog(configData->bskLogger, BSK_INFORMATION, info);

    /* initialize the output message to zero on reset */
    CModuleTemplateMsgPayload outMsgBuffer;
    outMsgBuffer = CModuleTemplateMsg_C_zeroMsgPayload();
    CModuleTemplateMsg_C_write(&outMsgBuffer, &configData->dataOutMsg, moduleID, callTime);
}
Use Reset to:
  • Reset integral or time-varying state to zero.
  • Perform one-time reads of configuration messages (spacecraft config, RW config, etc.).
  • Check that required input messages are linked and log BSK_ERROR if not.

Update

Update runs every time the simulation task fires. Always zero the output buffer first, then read inputs, compute, and write outputs.
/*! Add a description of what this main Update() routine does for this module

 @param configData The configuration data associated with the module
 @param callTime The clock time at which the function was called (nanoseconds)
 @param moduleID The module identifier
*/
void Update_cModuleTemplate(cModuleTemplateConfig *configData, uint64_t callTime, int64_t moduleID)
{
    double Lr[3];                                 /*!< [unit] variable description */
    CModuleTemplateMsgPayload outMsgBuffer;       /*!< local output message copy */
    CModuleTemplateMsgPayload inMsgBuffer;        /*!< local copy of input message */

    // always zero the output buffer first
    outMsgBuffer = CModuleTemplateMsg_C_zeroMsgPayload();
    v3SetZero(configData->inputVector);

    /*! - Read the optional input messages */
    if (CModuleTemplateMsg_C_isLinked(&configData->dataInMsg)) {
        inMsgBuffer = CModuleTemplateMsg_C_read(&configData->dataInMsg);
        v3Copy(inMsgBuffer.dataVector, configData->inputVector);
    }

    /*! - Add the module specific code */
    v3Copy(configData->inputVector, Lr);
    configData->dummy += 1.0;
    Lr[0] += configData->dummy;

    /*! - store the output message */
    v3Copy(Lr, outMsgBuffer.dataVector);

    /*! - write the module output message */
    CModuleTemplateMsg_C_write(&outMsgBuffer, &configData->dataOutMsg, moduleID, callTime);
}
Always zero the output message buffer at the start of Update. Writing stale or uninitialized data to a message is a common source of hard-to-diagnose bugs.

C message object API

The C message wrapper functions are auto-generated into dist3/autoSource/cMsgCInterface/. For a message type SomeMsg, the API is: Output message setup
FunctionDescription
SomeMsg_C_init(SomeMsg_C *msg)Initialize the output message object (call once in SelfInit)
SomeMsg_C_zeroMsgPayload()Return a zeroed payload struct
SomeMsg_C_write(payload*, msg*, moduleID, callTime)Write payload to the message
SomeMsg_C_isLinked(SomeMsg_C *msg)Returns 1 if a reader is connected
Input message reading
FunctionDescription
SomeMsg_C_read(SomeMsg_C *msg)Read and return a copy of the message payload
SomeMsg_C_isLinked(SomeMsg_C *msg)Returns 1 if connected to an output message
SomeMsg_C_isWritten(SomeMsg_C *msg)Returns 1 if the connected message has been written
SomeMsg_C_timeWritten(SomeMsg_C *msg)Returns write time in nanoseconds (uint64_t)
SomeMsg_C_moduleID(SomeMsg_C *msg)Returns the ID of the writing module

Writing the SWIG interface file

The .i file exposes your C module to Python.
// cModuleTemplate.i
%module cModuleTemplate

%include "architecture/utilities/bskException.swg"
%default_bsk_exception();

%{
   #include "cModuleTemplate.h"
%}

%include "swig_c_wrap.i"
%c_wrap(cModuleTemplate);

%include "architecture/msgPayloadDefC/CModuleTemplateMsgPayload.h"
struct CModuleTemplateMsg_C;

%pythoncode %{
import sys
protectAllClasses(sys.modules[__name__])
%}
The %c_wrap(moduleName) macro handles all the boilerplate for C modules — it binds the SelfInit, Reset, and Update functions and wraps the config structure so Python can instantiate and configure the module. The struct SomeMsg_C; forward declaration is required for every message type used. Without it, the code compiles but Python assignments to message variables silently have no effect. For additional Python interface capabilities, add these includes before %c_wrap:
IncludeEnables
%include "std_string.i"Module string variables
%include "std_vector.i"Standard vectors
%include "swig_eigen.i"Eigen vectors and matrices

Using the module from Python

Once built, you can import and use the module like this:
from Basilisk.utilities import SimulationBaseClass
from Basilisk.utilities import macros
from Basilisk.architecture import messaging
from Basilisk.moduleTemplates import cModuleTemplate

scSim = SimulationBaseClass.SimBaseClass()
dynProcess = scSim.CreateNewProcess("dynamicsProcess")
dynProcess.addTask(scSim.CreateNewTask("dynamicsTask", macros.sec2nano(1.0)))

# Instantiate the C module
mod = cModuleTemplate.cModuleTemplate()
mod.ModelTag = "myModule"
scSim.AddModelToTask("dynamicsTask", mod)

# Set module parameters
mod.dummy = 1
mod.dumVector = [1., 2., 3.]

# Create and connect an input message
inputData = messaging.CModuleTemplateMsgPayload()
inputData.dataVector = [1.0, -0.5, 0.7]
inputMsg = messaging.CModuleTemplateMsg().write(inputData)
mod.dataInMsg.subscribeTo(inputMsg)

# Record the output
dataLog = mod.dataOutMsg.recorder()
scSim.AddModelToTask("dynamicsTask", dataLog)

scSim.InitializeSimulation()
scSim.ConfigureStopTime(macros.sec2nano(1.0))
scSim.ExecuteSimulation()

print(dataLog.dataVector)

Variable number of input messages

To accept a variable number of input messages in a C module, declare a fixed-size array in the config struct:
SomeMsg_C moreInMsgs[10];   //!< array of up to 10 input messages
In Reset or Update, loop over the array and check SomeMsg_C_isLinked to determine how many slots are actually connected:
for (int i = 0; i < 10; i++) {
    if (SomeMsg_C_isLinked(&configData->moreInMsgs[i])) {
        inMsgBuffer = SomeMsg_C_read(&configData->moreInMsgs[i]);
        /* process inMsgBuffer */
    }
}
Use a fixed-size array rather than dynamic allocation to keep the module suitable for flight code targets.

Build docs developers (and LLMs) love