Using C++ with the NXP Zigbee SDK

last updated: Mar 31, 2025

The NXP Zigbee SDK is not well setup for use with C++. Although I prefer it over the Texas Instruments ZStack for various reasons, ease of C++ development is not one of them. I had no trouble converting ZStack sample programs to C++. I had all sorts of issues with the NXP SDK.

Although it wasn’t tooooo difficult to get things building, it took time and the process was frustrated by SDK bugs that only appear during a C++ build. Chiefly the lack of C++ guards in many headers and missing closing guards in a few. This latter problem is a true pain as it surfaces as closing brace errors in files a long way away from the source of the error.

Brace errors

Here’s a fine example:

middleware/wireless/framework/Common/dbg.h

#if defined __cplusplus
extern "C" {
#endif

#if (!defined JENNIC_CHIP_FAMILY_JN518x)

// Do some stuff and declare some functions

#if defined __cplusplus // OOPS!!!
};                      // Closing brace is defined
#endif                  // inside the the chip #ifdef

#else /* JENNIC_CHIP_FAMILY is JN518x */

// Do some different stuff and declare some other functions

#endif

This bug will trigger a closing brace error in a source file that includes a header that includes a header that includes a header…that includes this header.

An (incomplete) list of the brace problems:

1. Missing closing braces:

- middleware/wireless/zigbee/ZigbeeCommon/Include/appZpsBeaconHandler.h
- middleware/wireless/framework/RNG/Interface/rnd_pub.h
- middleware/wireless/zigbee/ZCL/Clusters/OTA/Include/OTA.h

2. Completely missing guard

- middleware/wireless/zigbee/ZigbeeCommon/Include/ZQueue.h
- middleware/wireless/zigbee/ZigbeeCommon/Include/ZTimer.h
- middleware/wireless/zigbee/ZPSAPL/Include/zps_apl_zdo.h
- middleware/wireless/zigbee/ZPSAPL/Include/zps_apl_af.h
- middleware/wireless/zigbee/ZPSAPL/Include/zps_apl_aib.h
- middleware/wireless/zigbee/ZPSAPL/Include/zps_apl_zdp.h
- middleware/wireless/zigbee/framework/PDUM/Include/pdum_apl.h
- middleware/wireless/zigbee/ZCL/Clusters/General/Include/Scenes.h
- middleware/wireless/zigbee/ZCL/Clusters/General/Include/OnOff.h
- middleware/wireless/zigbee/ZCL/Clusters/General/Include/Groups.h
- middleware/wireless/zigbee/ZCL/Clusters/General/Include/MultistateInputBasic.h
- middleware/wireless/zigbee/ZCL/Clusters/General/Include/Identify.h
- middleware/wireless/zigbee/ZCL/Clusters/General/Include/Basic.h
- middleware/wireless/zigbee/ZigbeeCommon/Include/portmacro_JN518x.h (probably also 517x, 516x)

3. Code generation tools

In addition to the SDK headers, the headers produced by the code generation tools (PDUMConfig and ZPSConfig) also lack C++ guards. Fixing this requires editing the Python scripts used for the code generation.

C++ startup code != C startup code

Typically in a mixed C/C++ project like this, we’d compile and link with gcc. GCC is smart enough to use g++ as needed when compiling .cpp files, and we don’t want the c++ stdlib so it’s fine for linking too.

This is all true, but there are two C++ specific issues that must be considered:

1 - Local static objects

Consider the following (real) example from my Aqara E1 Zigbee switch firmware code:

static Leds<config>& get()
{
    // lazy initialized
    static Leds<config> instance;
    return instance;
}

The instance variable is only constructed when the function is called for the first time and there is only ever one copy. In a concurrent environment, assistance is required to ensure that this is true.

Without protection, one can imagine a scenario where the initialization is interrupted

  • thread A calls Leds::get()
  • thread A begins constructing the object, but is interrupted by thread B
  • thread B also calls Leds::get()
  • [begin fireworks]

To prevent this, synchronization primitives are needed, and C++ uses the following:

  • __cxa_guard_acquire()
  • __cxa_guard_release()
  • __cxa_guard_abort()
  • __cxa_atexit()
  • __aeabi_atexit().

The linker will complain if there are static local objects and it can’t find these symbols. Including the C++ stdlib takes care of this, but it also adds ~60kb to the binary.

We have no need for this protection in this app. We could define empty implementations, but an easier solution is adding the following compiler flag:

-fno-threadsafe-statics

Do not emit the extra code to use the routines specified in the C++ ABI for thread-safe initialization of local statics. You can use this option to reduce code size slightly in code that doesn’t need to be thread-safe.

That brings us to the second problem.

2 - Global static objects

Global static objects must be constructed before the application begins (i.e. before main is called). Our example C project does not include code to do this, and adding some .cpp files and compiling/linking them with G++ does not magically add it.

Unlike the local static objects, the compiler/linker will not alert you to the fact that your global objects haven’t had their constructors called. The errors resulting from this will be obvious if you’re lucky, or subtle if you’re not.

The C runtime provides a function (__libc_init_array) to perform this initialization (and more). The startup file used in the NXP example program anticipates C++ development, and includes the function call:

startup_JN5189.c

...

#if defined(__cplusplus)
    //
    // Call C++ library initialisation
    //
    __libc_init_array();
#endif

#if defined(__REDLIB__)
    // Call the Redlib library, which in turn calls main()
    __main();
#else
    main();
#endif

...

The problem with this is that this file has a .c extension and so it won’t be compiled as C++ by GCC. The __cplusplus macro won’t be defined, and so the call won’t be made. We can’t use G++ to compile everything as C++ because the SDK has plenty of code that is not valid C++ (implicit pointer conversions, enums etc.), so it’s necessary to force C++ treatment for this specific file. I’m no makefile guru, but I put a conditional into the main compilation rule like this and it worked fine:

C_AS_CXX_FILES := startup_JN5189.c

$(APP_OBJ_DIR)/%.o: %.c
    $(info Compiling C file $< into $@...)
    @if echo $(C_AS_CXX_FILES) | grep -q $(notdir $<); then \
        echo "Forcing C++ compilation for "$(notdir $<); \
        $(TOOLCHAIN_PATH)/$(CXX) -c -o $@ $(CFLAGS) $(CXXFLAGS) $(INCFLAGS) $< -MD -MF $(APP_OBJ_DIR)/$*.d -MP; \
    else \
        $(TOOLCHAIN_PATH)/$(CC) -c -o $@ $(CFLAGS) $(INCFLAGS) $< -MD -MF $(APP_OBJ_DIR)/$*.d -MP; \
    fi

Almost there

This gets us most of the way. The startup code will now call __libc_init_array, which is provided by the C runtime. For the GCC ARM compiler, this means Newlib. The function looks like this:

Newlib source file: newlib/libc/misc/init.c

/*
 * Copyright (C) 2004 CodeSourcery, LLC
 *
 * Permission to use, copy, modify, and distribute this file
 * for any purpose is hereby granted without fee, provided that
 * the above copyright notice and this notice appears in all
 * copies.
 *
 * This file is distributed WITHOUT ANY WARRANTY; without even the implied
 * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
 */

/* Handle ELF .{pre_init,init,fini}_array sections.  */
#include <sys/types.h>

#ifdef _HAVE_INITFINI_ARRAY

/* These magic symbols are provided by the linker.  */
extern void (*__preinit_array_start []) (void) __attribute__((weak));
extern void (*__preinit_array_end []) (void) __attribute__((weak));
extern void (*__init_array_start []) (void) __attribute__((weak));
extern void (*__init_array_end []) (void) __attribute__((weak));

#ifdef _HAVE_INIT_FINI
extern void _init (void);
#endif

/* Iterate over all the init routines.  */
void
__libc_init_array (void)
{
  size_t count;
  size_t i;

  count = __preinit_array_end - __preinit_array_start;
  for (i = 0; i < count; i++)
    __preinit_array_start[i] ();

#ifdef _HAVE_INIT_FINI
  _init ();
#endif

  count = __init_array_end - __init_array_start;
  for (i = 0; i < count; i++)
    __init_array_start[i] ();
}
#endif

We can see that it loops through two arrays of function pointers, executing each function in order. The compiler adds the static init startup code to the __init_array. For things to work, this section needs to be defined.

.preinit_array:
This section holds an array of function pointers that contributes to a single pre-initialization array for the executable or shared object containing the section.

.init_array
This section holds an array of function pointers that contributes to a single initialization array for the executable or shared object containing the section.

System V ABI

The NXP linker script does not contain these sections

The app note example uses the script located at: [SDK]/boards/jn5189dk6/wireless_examples/zigbee/zigbee_router/bm/AppBuildNone_JN5189.ld, which lacks these sections :-(.

The JN5189_flash.ld linker script in the SDK shows how all of the C++ related sections should be correctly included:

 .ctors :
  {
    __CTOR_LIST__ = .;
    /* gcc uses crtbegin.o to find the start of
       the constructors, so we make sure it is
       first.  Because this is a wildcard, it
       doesn't matter if the user does not
       actually link against crtbegin.o; the
       linker won't look for a file to match a
       wildcard.  The wildcard also means that it
       doesn't matter which directory crtbegin.o
       is in.  */
    KEEP (*crtbegin.o(.ctors))
    KEEP (*crtbegin?.o(.ctors))
    /* We don't want to include the .ctor section from
       from the crtend.o file until after the sorted ctors.
       The .ctor section from the crtend file contains the
       end of ctors marker and it must be last */
    KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .ctors))
    KEEP (*(SORT(.ctors.*)))
    KEEP (*(.ctors))
    __CTOR_END__ = .;
  } > m_text

  .dtors :
  {
    __DTOR_LIST__ = .;
    KEEP (*crtbegin.o(.dtors))
    KEEP (*crtbegin?.o(.dtors))
    KEEP (*(EXCLUDE_FILE(*crtend?.o *crtend.o) .dtors))
    KEEP (*(SORT(.dtors.*)))
    KEEP (*(.dtors))
    __DTOR_END__ = .;
  } > m_text

  .preinit_array :
  {
    PROVIDE_HIDDEN (__preinit_array_start = .);
    KEEP (*(.preinit_array*))
    PROVIDE_HIDDEN (__preinit_array_end = .);
  } > m_text

  .init_array :
  {
    PROVIDE_HIDDEN (__init_array_start = .);
    KEEP (*(SORT(.init_array.*)))
    KEEP (*(.init_array*))
    PROVIDE_HIDDEN (__init_array_end = .);
  } > m_text

  .fini_array :
  {
    PROVIDE_HIDDEN (__fini_array_start = .);
    KEEP (*(SORT(.fini_array.*)))
    KEEP (*(.fini_array*))
    PROVIDE_HIDDEN (__fini_array_end = .);
  } > m_text

For our case, we only need init_array, and so the following change to the example script is sufficient:

AppBuildNone_JN5189_with_init_array.ld

...

.text : ALIGN(0x40)
{
    FILL(0xff)

    *(.after_vectors*)
    *(.text*)

   /*
    * MODIFICATION - BEGIN
    */

    . = ALIGN(4);
    __init_array_start = .;
    KEEP(*(.init_array))
    KEEP(*(.ctors))
    __init_array_end = .;

   /*
    * MODIFICATION - END
    */

    *(.rodata .rodata.* .constdata .constdata.*)
    . = ALIGN(4);
} > Flash640

...

One last thing

We also need to add an empty _init function definition to avoid a missing symbol error during the link.

void _init(void) { }

_init is provided by the linker startup files, which are excluded from the build with the --nostartfiles flag. The function can be empty because the initialization is done by the __libc_init_array function in the C runtime.

If your’re writing a baremetal application and not using the C stdlib, you can just add the initialization code yourself in your own _init function.

And…..done!

With these changes, I was able to successfully build and deploy a C++ application for the JN5189.