(11-4-2025) BLE Stack in C++

I wrote a blog on writing modular code in C and now, for no reason at all, decided to see about sit'n a spell and do'n 'er up in C++. Why not Rust you say? Well yes, Rust has a crab for a logo and yet another crab movie is about to...molt? But C++ has more support in embedded systems, although Rust is supported in nRF Connect...No! C++. Now let's refer back to my wonderful BLE stack diagram.

My intent here is to make BLE interface, packet translator and application classes, then just use the same SerialPort.c file from the other project...probably should have made that a class too; oh well, code's done can't change it.

But What Class?

Keep in mind my C++ experience pre-dates C++11, so been a while. So I figured, make some classes and...hmf. In the C code, the Application module can set it's BleInterface module callbacks while unbeknownst, the NordicPacketTranslator module sidles in and sets it's callbacks. Doing that in C++ requires the Application and NordicPacketTranslator, now classes, to grab the same instance of the BleInterface class. Just because I've been outa C++ doesn't mean I've not been Java'n it up on Android, and we, meaning Yoan I think, made a singleton that I used. Can such a thing exist in C++? You bet, but so does dependency injection. Maybe I should have used straight dependency injection. Oh well, code's done can't change it.

The Most Famous Singleton of All

The Meyers Singleton. Brought to you by Scott Meyers who is still alive. Yea! This singleton delivers pure driving excitement, sleek good looks with a responsive 4-cylinder engine...wait...responsive? In what way? This is a Pontiac 2000 we're talking about Bob. That thing can't get out of it's own way. All inside Frosty the Snowman/Price is Right jokes aside, not without controversy, the Meyers Singleton uses a local static instance and disables copy constructor/assignment operations while making the constructor private to prevent creation of multiple instances. I made the BleInterface a Meyers Singleton. Behold.

class BleInterface { public: // Prevent copying (singleton-like behavior). BleInterface(const BleInterface&) = delete; BleInterface& operator=(const BleInterface&) = delete; BleInterface(BleInterface&&) = delete; BleInterface& operator=(BleInterface&&) = delete; // Get the singleton-like instance; static BleInterface& instance(); // Application callbacks void SetBleAppCallbacks(BleIntAppCallbacks* appCb) { m_app = appCb; } // Packet Translator callbacks void SetBlePktTransCallbacks(BleIntPktTransCallbacks* pktTransCb) { m_pkt_trans = pktTransCb; } // Public API void StartAdv(void); void WriteBleAttribute(TS_BLE_ATTRIBUTE_DATA *pAttrData); void PostBleResp(TE_PACKET_TRANSLATOR_RESP_ID respId, uint8_t isSuccess); private: BleInterface() = default; ~BleInterface(); BleIntAppCallbacks* m_app = nullptr; BleIntPktTransCallbacks* m_pkt_trans = nullptr; };

What does this singleton get us? Well inability to destroy all contained resources until program termination which is why Mr. YouTube doesn't like them. But also that public instance() function returns the single, statically allocated instance every time it's called from anywhere in the program.

BleInterface& BleInterface::instance() { static BleInterface singleton; return singleton; }

And public functions SetBleAppCallbacks() and SetBlePktTransCallbacks() allow code from anywhere to set the desired callbacks. AND those callbacks are kindof abstract classes but not really because they don't have at least one pure virtual function because I did not want to force the inheriting class to implement all virtual functions because real world implementations may not. The AI calls them "base callback interfaces" or "non-pure virtual interfaces". So we shall all call them what the AI says, for the singularity is coming; it's a singleton.

But you're Setting Everything Up in One Place

Exactly. A real pickle. In the C implementation, NordicPacketTranslator.c calls SetPacketTransCallbacks() and Application.c calls SetBleAppCallbacks(). C++ is doing it all in main().

int main() { char choice = 0; TS_NORDIC_PT_SETUP ptSetup; ptSetup.pSendBytes = SerialPortSendBytes; ptSetup.pGetBytes = SerialPortGetBytes; NordicPacketTranslator nordicPktTrans(ptSetup); Application app; auto& bleInterface = BleInterface::instance(); bleInterface.SetBlePktTransCallbacks(&nordicPktTrans); bleInterface.SetBleAppCallbacks(&app); ... }

In my defense, if I tried dependency injecting a BleInterface object into the Application and NordicPacketTranslator classes, I'd still have to do that all in one place maybe. Also, the C implementation is calling InitNordicPacketTranslator() and InitApplication() in main, so I think the moral of the story is just use global variables.

Just a Little Bit Extra

Supreme confidence in my C implementation lead me to just write non-functioning example code. Supreme lack of confidence in this C++ version lead me to make a functional MS Visual Studio console program that takes some simple user input and outputs status in all the right places. It even replicates Mr. Cherno's gripe about the whole destroying after program termination thing, although isn't that going to happen anyway unless you expressly delete a dynamically heap allocated class instance? I say yes because I'm allocating Application and NordicPacketTranslator instances on the stack to be automatically destroyed when main() goes out of scope.

Thoughts?

The post-termination destruction doesn't matter for this implementation because embedded systems stay up until power is removed or the system resets. Otherwise, stepping through the program, I anecdotally think it's doing more cycles to invoke the callback functions, which makes sense as it's all objecty. I find the syntax confusing, but that could be from not writing C++ in a long while. Some say this should make it easier to write unit tests, but I'm not so sure; I'd need to try it. Overall, I'd need to implement a full system or two in C++ before judging. For now, I gotta crab movie to go see.