Shared libraries are our best friends to extend the functionality of C programs without reinventing the wheel. They offer a collection of exported functions, variables, and other symbols that we can use inside our own program as if the content of the shared library was a direct part of our code. The usual way to use such libraries is to simply link against them at compile time, and let the linker resolve all external symbols and make sure everything is in place when creating our executable file. Whenever we then run our executable, the loader, a part of the operating system, will try to resolve again all the symbols, and load every required library into memory, along with our executable itself.
But what if we didn’t want to add libraries at compile time, but instead load them ourselves as needed during runtime? Instead of a predefined dependency on a library, we could make its presence optional and adjust our program’s functionality accordingly. Well, we can do just that with the concept of dynamic loading. In this article, we will look into dynamic loading, how to use it, and what to do with it — including building our own plugin system. But first, we will have a closer look at shared libraries and create one ourselves.
But data isn’t the only thing residing in memory. All the program code is accessible through either the RAM or some other executable type of memory, giving each function a specific address inside that memory as entry point. Once again, pointers are simply memory addresses, and to fully utilize this similarity, C provides the concept of function pointers. Function pointers provide us with ways to make conditional code execution faster, implement callbacks to make code more modular, and even provide a foothold into the running machine code itself for reverse engineering or exploitation. So read on!
In general, function pointers aren’t any more mysterious than data pointers: the main difference is that one references variables and the other references functions. If you recall from last time how arrays decay into pointers to their first element, a function equally decays into a pointer to the address of its entry point, with the () operator executing whatever is at that address. As a result, we can declare a function pointer variable fptr and assign a function func() to it: fptr = func;. Calling fptr(); will then resolve to the entry point of function func() and execute it.
Admittedly, the idea of turning a function into a variable may seem strange at first and might require some getting used to, but it gets easier with time and it can be a very useful idiom. The same is true for the function pointer syntax, which can be intimidating and confusing in the beginning. But let’s have a look at that ourselves.
In our first part on pointers, we covered the basics and common pitfalls of pointers in C. If we had to break it down into one sentence, the main principle of pointers is that they are simply data types storing a memory address, and as long as we make sure that we have enough memory allocated at that address, everything is going to be fine.
In this second part, we are going to continue with some more advanced pointer topics, including pointer arithmetic, pointers with another pointer as underlying data type, and the relationship between arrays and pointers. But first, there is one particular pointer we haven’t talked about yet.
The one proverbial exception to the rule that pointers are just memory addresses is the most (in)famous pointer of all: the NULL pointer. Commonly defined as preprocessor macro (void *) 0, we can assign NULL like any other pointer.
Pointers — you either love them, or you haven’t fully understood them yet. But before you storm off to the comment section now, pointers are indeed a polarizing subject and are both C’s biggest strength, and its major source of problems. With great power comes great responsibility. The internet and libraries are full of tutorials and books telling about pointers, and you can randomly pick pretty much any one of them and you’ll be good to go. However, while the basic principles of pointers are rather simple in theory, it can be challenging to fully wrap your head around their purpose and exploit their true potential.
So if you’ve always been a little fuzzy on pointers, read on for some real-world scenarios of where and how pointers are used. The first part starts with regular pointers, their basics and common pitfalls, and some general and microcontroller specific examples.
Programming C without variables is like, well, programming C without variables. They are so essential to the language that it doesn’t even require an analogy here. We can declare and use them as wildly as we please, but it often makes sense to have a little bit more structure, and combine data that belongs together in a common collection. Arrays are a good start to bundle data of the same type, especially when there is no specific meaning of the array’s index other than the value’s position, but as soon as you want a more meaningful association of each value, arrays will become limiting. And they’re useless if you want to combine different data types together. Luckily, C provides us with proper alternatives out of the box.
This write-up will introduce structures and unions in C, how to declare and use them, and how unions can be (ab)used as an alternative approach for pointer and bitwise operations.
[Scott Harden] is working on a research project involving optogenetics. From what we were able to piece together optogenetics is like this: someone genetically modifies a mouse to have cell behaviors which can activated by light sensitive proteins. The mice then have a frikin’ lasers mounted on their heads, but pointing inwards towards their brains not out towards Mr. Bond’s.
Naturally, to make any guesses about the resulting output behavior from the mouse the input light has to be very controlled and exact. [Scott] had a laser and he had a driver, but he didn’t have a controller to fire the pulses. To make things more difficult, the research was already underway and the controller had to be built
The expensive laser driver had a bizarre output of maybe positive 28 volts or, perhaps, negative 28 volts… at eight amps. It was an industry standard in a very small industry. He didn’t have a really good way to measure or verify this without either destroying his measuring equipment or the laser driver. So he decided to just build a voltage-agnostic input on his controller. As a bonus the opto-isolated input would protect the expensive controller.
The output is handled by an ATtiny85. He admits that a 555 circuit could generate the signal he needed, but to get a precision pulse it was easier to just hook up a microcontroller to a crystal and know that it’s 100% correct. Otherwise he’d have to spend all day with an oscilloscope fiddling with potentiometers. Only a few Hackaday readers relish the thought as a relaxing Sunday afternoon.
He packaged everything in a nice project box. He keeps them on hand to prevent him from building circuits on whatever he can find. Adding some tricks from the ham-radio hobby made the box look very professional. He was pleased and surprised to find that the box worked on his first try.
An icon of Computer Science, [Edsger Dijkstra], published a letter in the Communications of the Association of Computer Machinery (ACM) which the editor gave the title “Go To Statement Considered Harmful“. A rousing debate ensued. A similar criticism of macros, i.e. #define, in C/C++ may not rise to that level but they have their own problems.
Macros are part of the preprocessor for the C/C++ languages which manipulates the source code before the actual translation to machine code. But there are risks when macros generate source code. [Bjarne Stroustrup] in creating C++ worked to reduce the need and usage of the preprocessor, especially the use of macros. In his book, The C++ Programming Language he writes,
Don’t use them if you don’t have to. Almost every macro demonstrates a flaw in the programming language, in the program, or in the programmer.
As C retrofitted capabilities of C++, it also reduced the need for macros, thus improving that language.
With the Arduino using the GNU GCC compilers for C and C++ I want to show new coders a couple of places where the preprocessor can cause trouble for the unwary. I’ll demonstrate how to use language features to achieve the same results more cleanly and safely. Of course, all of this applies equally when you use any of these languages on other systems.