Part 1: Create a Plugin

A plugin mechanism in OpenAirInterface will allow us to isolate functionality originally implemented in the main source code and modularize it, so that multiple implementations and extensions to the functionality can be done. Using dynamic libraries, we will be able to select across multiple modules implementing the functionality based on the use case needs.

To keep the implementation of new functions flexible, we use OpenAirInterface’s dynamic module loader [OAILib] which allows to load different implementations at runtime. This requires creating a plugin, but also adding the required hooks or entry points in the original code base to initialize, make them available and use the plugin. Further, modifications to the build system and the container images created are needed.

The global modifications are already integrated in the patched OpenAirInterface code base. We will focus on the tasks required to add a plugin in this added infrastructure.

For this tutorial, we will show the procedure how the QAM demapper is modularized. The first part will describe the different parts of the plugin, and the second part will extend it with a second version that captures data from the selected function. Further, the Integration of a Neural Demapper in the next tutorial will be implemented as a plugin and can be used as a drop-in replacement for the existing OAI demapper.

Note

The current plugin mechanism works with L1 code. Other parts of the OAI code, like L2, require some modifications and are still work in progress.

Architecture Overview

There is a set of global changes to the OpenAirInterface needed to compile and expose the plugins in the code base. These changes involve exposing the plugin headers to the codebase, connecting the initialization functions, as well as changes to the build system. These changes are not covered by the tutorial.

The global files plugins.h, plugins.c and CMakeLists.txt represent the entry points of this infrastructure that are modified when adding a new plugin.

Plugin specific files cover the following parts:

  • Type definitions. Data types and function signatures exposed by the plugin to OAI.

  • Public interface. Set of functions exposed by the dynamic library, as well as the global entry point of the interface.

  • Init functions. Functions called during the initialization of OAI that loads the dynamic library and populates the global entry point with the corresponding function pointers. Additional functions for proper deinitialization as well as additional setups.

  • Plugin entry point. A globally accessible structure (a singleton) that contains the function pointers to the functions of the plugin to be used inside the OAI codebase.

  • Plugin code. The code that implements the plugin functionality.

File

Description

plugins.h

Header entry point, all plugins headers added here

plugins.c

Plugin entry point, wires loading, unloading and init of all plugins

_defs.h

Types definitions and function signatures

_extern.h

Public interface of the plugin

_load.c

Load / Unload / Init functions

.c

Plugin compilation units

CMakeLists.txt

CMake build files

Select Functions

We focus on modularizing the demapper function nr_ulsch_compute_llr into a separate module. This function, found in openair1/PHY/NR_TRANSPORT/nr_ulsch_llr_computation.c, handles the demapping of received QAM symbols to log-likelihood ratios (LLRs) based on the modulation scheme used. It has a clear and well-defined interface, making it an ideal candidate for modularization. The function’s signature will serve as the basis for our plugin’s interface requirements.

Listing 2 Functions to extract / nr_ulsch_llr_computation.c
 1void nr_ulsch_compute_llr(int32_t *rxdataF_comp,
 2                        int32_t *ul_ch_mag,
 3                        int32_t *ul_ch_magb,
 4                        int32_t *ul_ch_magc,
 5                        int16_t *ulsch_llr,
 6                        uint32_t nb_re,
 7                        uint8_t  symbol,
 8                        uint8_t  mod_order)
 9{
10    switch(mod_order){
11        case 2:
12        nr_ulsch_qpsk_llr(rxdataF_comp,
13                            ulsch_llr,
14                            nb_re,
15                            symbol);
16        break;
17        case 4:
18        nr_ulsch_16qam_llr(rxdataF_comp,
19                            ul_ch_mag,
20                            ulsch_llr,
21                            nb_re,
22                            symbol);
23        break;
24        case 6:
25        nr_ulsch_64qam_llr(rxdataF_comp,
26                        ul_ch_mag,
27                        ul_ch_magb,
28                        ulsch_llr,
29                        nb_re,
30                        symbol);
31        break;
32        case 8:
33        nr_ulsch_256qam_llr(rxdataF_comp,
34                            ul_ch_mag,
35                            ul_ch_magb,
36                            ul_ch_magc,
37                            ulsch_llr,
38                            nb_re,
39                            symbol);
40        break;
41        default:
42        AssertFatal(1==0,"nr_ulsch_compute_llr: invalid Qm value, symbol = %d, Qm = %d\n",symbol, mod_order);
43        break;
44    }
45}

Define Module Interface

Next, we define the module interface that specifies the function we want to extend, along with additional functions for module initialization and cleanup during loading and unloading.

Listing 3 nr_demapper_extern.h
 1#include "nr_demapper_defs.h"
 2
 3typedef struct demapper_interface_s {
 4    demapper_initfunc_t        *init;
 5    demapper_initfunc_t        *init_thread;
 6    demapper_shutdownfunc_t    *shutdown;
 7    demapper_compute_llrfunc_t *compute_llr;
 8} demapper_interface_t;
 9
10// global access point for the plugin interface
11extern demapper_interface_t demapper_interface;
12
13int load_demapper_lib( char *version, demapper_interface_t * );
14int free_demapper_lib( demapper_interface_t *demapper_interface );
15
16demapper_initfunc_t        demapper_init;
17demapper_shutdownfunc_t    demapper_shutdown;
18demapper_compute_llrfunc_t demapper_compute_llr;

The structure demapper_interface_t defines the functions that each dynamically loaded library will export, making them available for dynamic linking at runtime. The global instance demapper_interface is the actual mapping loaded and active in the system - this serves as the entry point that makes the module available throughout the codebase. The functions load_demapper_lib and free_demapper_lib are called during initialization to select and load the library, utilizing the module loader functionality.

The actual function signatures are defined in a separate file nr_demapper_defs.h given as:

Listing 4 nr_demapper_defs.h
 1typedef int32_t(demapper_initfunc_t)(void);
 2typedef int32_t(demapper_shutdownfunc_t)(void);
 3
 4typedef int(demapper_compute_llrfunc_t)( int32_t *rxdataF_comp,
 5                          int32_t *ul_ch_mag,
 6                          int32_t *ul_ch_magb,
 7                          int32_t *ul_ch_magc,
 8                          int16_t *ulsch_llr,
 9                          uint32_t nb_re,
10                          uint8_t  symbol,
11                          uint8_t  mod_order );

Loading the Module

Loading the module happens during the initialization of the application. We leverage OAI’s config utility, which provides flexibility in specifying the libraries to load through configuration options. This can be done in either a file or as command line arguments.

The load function performs dynamic linking of the selected module and retrieves the function pointers that the module exports. These function pointers are then mapped to the corresponding elements of the global entry point interface, making them available throughout the application.

The unload function handles the graceful shutdown of the module according to the module’s own cleanup specifications.

Listing 5 nr_demapper_load.c
 1static int32_t demapper_no_thread_init() {
 2    return 0;
 3}
 4
 5int load_demapper_lib( char *version, demapper_interface_t *interface )
 6{
 7    char *ptr = (char*)config_get_if();
 8    char libname[64] = "demapper";
 9
10    if (ptr == NULL) {  // config module possibly not loaded
11        uniqCfg = load_configmodule( 1, demapper_arg, CONFIG_ENABLECMDLINEONLY );
12        logInit();
13    }
14
15    // function description array for the shlib loader
16    loader_shlibfunc_t shlib_fdesc[] = { {.fname = "demapper_init" },
17                                         {.fname = "demapper_init_thread", .fptr = &demapper_no_thread_init },
18                                         {.fname = "demapper_shutdown" },
19                                         {.fname = "demapper_compute_llr" }};
20
21    int ret;
22    ret = load_module_version_shlib( libname, version, shlib_fdesc, sizeofArray(shlib_fdesc), NULL );
23    AssertFatal((ret >= 0), "Error loading demapper library");
24
25    // assign loaded functions to the interface
26    interface->init = (demapper_initfunc_t *)shlib_fdesc[0].fptr;
27    interface->init_thread = (demapper_initfunc_t *)shlib_fdesc[1].fptr;
28    interface->shutdown = (demapper_shutdownfunc_t *)shlib_fdesc[2].fptr;
29    interface->compute_llr = (demapper_compute_llrfunc_t *)shlib_fdesc[3].fptr;
30
31    AssertFatal( interface->init() == 0, "Error starting Demapper library %s %s\n", libname, version );
32
33    return 0;
34}
35
36int free_demapper_lib( demapper_interface_t *demapper_interface )
37{
38    return demapper_interface->shutdown();
39}

To integrate these functions into the executables, we need to make the module interface visible and initialize it when the application starts. We accomplish this by adding them to the two global entry points:

  • tutorials/plugins.h: Contains the required module interface headers

  • tutorials/plugins.c: Implements the initialization calls

These files provide a single, clean entry point for module integration. They are incorporated into both the gNB and UE executables of OpenAirInterface, as well as the build system. Additional plugins only need to added to these two files to be exposed to OAI.

Listing 6 plugins.h
1#include "neural_demapper/nr_demapper_extern.h"
2
3void init_plugins();
4void free_plugins();
5
6void worker_thread_plugin_init();
Listing 7 plugins.c
 1// global entry points for the tutorial plugins
 2// any global structures defined here.
 3
 4demapper_interface_t demapper_interface = {0};
 5
 6
 7void init_plugins()
 8{
 9    // insert your plugin init here.
10
11    load_demapper_lib( NULL, &demapper_interface);
12}
13
14void free_plugins()
15{
16    // insert your plugin release/free here.
17
18    free_demapper_lib(&demapper_interface);
19}
20
21void worker_thread_plugin_init()
22{
23    // insert your plugin per thread initialization here.
24
25    if (demapper_interface.init_thread)
26        demapper_interface.init_thread();
27}

Use Module Functions

At this point, the module is loaded and its interface is available in the OpenAirInterface code. The next step is to use this interface. The demapper function selects between several modulation schemes and calls the corresponding implementation. Since we only want to accelerate specific modulation schemes, we will rename the original function nr_ulsch_compute_llr to nr_ulsch_compute_llr_default and implement a wrapper that falls back to this default implementation when the module does not handle a particular case.

Listing 8 Wrapper / nr_ulsch_llr_computation.c
 1// will include the definition for demapper_interface singleton
 2#include <tutorials/plugins.h>
 3
 4void nr_ulsch_compute_llr(int32_t *rxdataF_comp,
 5                        int32_t *ul_ch_mag,
 6                        int32_t *ul_ch_magb,
 7                        int32_t *ul_ch_magc,
 8                        int16_t *ulsch_llr,
 9                        uint32_t nb_re,
10                        uint8_t  symbol,
11                        uint8_t  mod_order)
12{
13    int handled = demapper_interface.compute_llr(rxdataF_comp,
14                                                ul_ch_mag,
15                                                ul_ch_magb,
16                                                ul_ch_magc,
17                                                ulsch_llr,
18                                                nb_re,
19                                                symbol,
20                                                mod_order);
21    if (!handled)
22        nr_ulsch_compute_llr_default(rxdataF_comp,
23                                    ul_ch_mag,
24                                    ul_ch_magb,
25                                    ul_ch_magc,
26                                    ulsch_llr,
27                                    nb_re,
28                                    symbol,
29                                    mod_order);
30}

Module Implementation

Now we can implement the actual functionality of the module. The implementation consists of two parts: a header file defining the interface and a source file containing its actual implementation. In this example, we take a minimal approach where the module simply returns zero, indicating that it does not handle any cases itself. Instead, it relies on the wrapper function to call the default behavior. This design pattern helps reduce code duplication while allowing the plugin to gradually take over specific cases as needed, with unhandled cases automatically falling back to the original implementation.

Listing 9 nr_demapper_orig.c
 1int32_t demapper_init( void )
 2{
 3    printf("Original/pass-through demapping initialized\n");
 4    // do nothing
 5    return 0;
 6}
 7
 8int32_t demapper_shutdown( void )
 9{
10    // do nothing
11    return 0;
12}
13
14// No custom code, trigger default handling by returning 0 for unhandled
15
16int demapper_compute_llr(int32_t *rxdataF_comp,
17                         int32_t *ul_ch_mag,
18                         int32_t *ul_ch_magb,
19                         int32_t *ul_ch_magc,
20                         int16_t *ulsch_llr,
21                         uint32_t nb_re,
22                         uint8_t  symbol,
23                         uint8_t  mod_order) {
24    // do nothing
25    return 0;
26}

Compiling

Finally, we need to compile our new plugin. This is done by creating a new CMakeLists.txt file in our module directory and including it from the main tutorials/CMakeLists.txt file. This setup allows CMake to properly build and link our plugin as a shared library. A minimal version looks as follows:

 1set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR})
 2
 3# demapper libraries
 4
 5# original
 6set(PHY_DEMAPPER_ORIG_SRC
 7  nr_demapper_orig.c
 8)
 9
10add_library(demapper_orig MODULE ${PHY_DEMAPPER_ORIG_SRC})
11add_library(demapper MODULE ${PHY_DEMAPPER_ORIG_SRC})
12
13add_dependencies(nr-softmodem demapper_orig demapper)
14add_dependencies(nr-uesoftmodem demapper_orig demapper)
15add_dependencies(demapper generate_T)
16add_dependencies(demapper_orig generate_T)

Incremental Builds

Rebuilding the full ran-build-cuda OAI image every time to rebuild only the module is too time consuming. You can however use the image to rebuild it quickly.

# Create temp directory
mkdir /tmp/demapper

# Launch a container, mounting the tutorials directory with the newer code.
# If you made changes to openairinterface5g code, you also need to mount those.
# for example, for openair1, add -v ext/openairinterface5g/openair1:/oai-ran/openair1
docker run -v /tmp/demapper:/mnt -v ./tutorials:/oai-ran/tutorials --rm -it ran-build-cuda:latest bash

# Inside the container
cmake -S . -B cmake_targets/ran_build/build/ -GNinja
cmake --build cmake_targets/ran_build/build/ --target demapper
cmake --build cmake_targets/ran_build/build/ --target demapper_orig

# Check updated libraries
ls -lh cmake_targets/ran_build/build/libdemapper*

# Copy resulting libraries to host
cp cmake_targets/ran_build/build/libdemapper* /mnt/

Container Changes

As a final step, you need to modify the gNB and UE Dockerfiles to include the newly created libraries in your containers. Here are the corresponding lines for the demapper:

Listing 10 gNB Docker file
# ...
COPY --from=gnb-build \
    /oai-ran/cmake_targets/ran_build/build/libdemapper*.so \
# ...
ldd /usr/local/lib/liboai_eth_transpro.so \
    /usr/local/lib/libdemapper*.so \
# ...
Listing 11 NR-UE Docker file
# ...
COPY --from=nr-ue-build \
    /oai-ran/cmake_targets/ran_build/build/libdemapper*.so \
# ...
ldd /usr/local/lib/liboai_eth_transpro.so \
    /usr/local/lib/libdemapper*.so \
# ...

We can now rebuild the source tree with the demapper functions modularized into a dynamically linked library. If multiple modules are available, they can be selected at runtime using --loader.demapper.shlibversion _orig for the original version. Alternatively, you can add the following line to the .env file in your configuration directory:

GNB_EXTRA_OPTIONS="--loader.demapper.shlibversion _orig"

When the library is loaded, you will see a message like this in the gNB logs:

[LOADER] library libdemapper_orig.so successfully loaded