This chapter describes several different programming options for the Beagle boards, including scripted and compiled languages. An LED flashing example is provided in all the languages so that you can investigate each language's structure and syntax. The advantages and disadvantages of each language are discussed along with example uses. The chapter then focuses on the C/C++ programming languages, describing their principles and why object-oriented programming (OOP) is appropriate and necessary for the development of scalable embedded systems applications. Finally, the chapter details how you can interface directly to the Linux kernel using the GNU C library. A single chapter can only scratch the surface on this topic, so this one focuses on physical programming tasks, which are required throughout the remainder of this book.
EQUIPMENT REQUIRED FOR THIS CHAPTER:
As discussed in Chapter 3, embedded Linux is essentially “Linux on an embedded system.” If your favorite programming language is available under Linux, then it is also likely to be available for the Beagle boards. So, is your favorite language suitable for programming your board? That depends on what you intend to do with the board. Are you interfacing to electronics devices/modules? Do you plan to write rich user interfaces? Are you planning to write a device driver for Linux? Is performance important, or are you developing an early pre-prototype? Each of the answers to these questions will impact your decision regarding which language you should use. In this chapter, you are introduced to several different languages, and the advantages and disadvantages of each category of language are outlined. As you read through the chapter, try to avoid focusing on a favorite language, but instead use the correct language for the job at hand.
How does programming on embedded systems compare to programming on desktop computers? Here are some points to consider:
For the upcoming discussion, it is assumed you are planning to do some type of physical computing—that is, interfacing to the different input or outputs on your board. Therefore, the example that is used to describe the structure and syntax of the different languages is a simple interfacing example. Before looking at the languages themselves, we will begin with a brief performance evaluation of different languages running on the Beagle platform in order to put the following discussions in context.
Which language is the best on the Beagle platform? Well, that is an incredibly emotive and difficult question to answer. Different languages perform better on different benchmarks and different tasks. In addition, a program written in a particular language can be optimized for that language to the point that it is barely recognizable as the original code. Nor is speed of execution always an important factor; you may be more concerned with memory usage, the portability of the code, or the ability to quickly apply changes.
However, if you are planning to develop high-speed or real-time number-crunching applications, then performance may be a key factor in your choice of programming language. In addition, if you are setting out to learn a new language and you may possibly be developing algorithmically rich programs in the future, then it may be useful to keep performance in mind.
A simple test has been put in place to determine the CPU performance of the languages discussed in this chapter. The test uses the n-body benchmark (gravitational interaction of planets in the solar system) code from benchmarksgame.alioth.debian.org
. The code uses the same algorithm for all languages, and the board is running in the same state in all cases. The test uses 5 million iterations of the algorithm to ensure that the script used for timing does not need to be highly accurate. All of the programs gave the same correct result, indicating that these all ran correctly and to completion. The test is available in the book's Git repository in the directory chp05/performance/
. Note that you must have installed Java and other languages on your board to run all of the tests. Use the following call to execute the test:
/chp05/performance$ ./run
Running the Tests:
The C/C++ Code Example
-0.169075164 -0.169083134
It took 34 seconds to run the C/C++ test …
Finished Running the Benchmarks
The results of the tests are displayed in Table 5-1. In the first column, you can see the results for the PocketBeagle, running at its top processor frequency of 1 GHz. For this number-crunching application, the general-purpose language, C++, performs the task in 34 seconds. This time has been weighted as one unit. Therefore, Java takes 1.15 times longer to complete the same task, the highly optimized Rust completes the task more quickly than C++, Node.js (for BoneScript) is 1.24 times longer, Perl takes 26.8 times longer, and Python takes 57 times longer. The processing durations in seconds are provided in parentheses. As you move across the columns, you can see that this performance is relatively consistent, even as the processor frequency is adjusted (discussed in the next section) or a desktop i7 64-bit processor is used.
Table 5-1: Numerical Computation Time for 5,000,000 Iterations of the n-Body Algorithm on a BeagleBoard.org Debian Stretch Hard-float Image
VALUE | TYPE | POCKETBEAGLE1 AT 1 GHZ | POCKETBEAGLE AT 600 MHZ | INTEL 64-BIT I7 DEBIAN PC2 |
C++3 | Compiled | 1.00× (34 s) | 1.00× (57 s) | 1.00× (0.641 s) |
C (gcc) | Compiled | 0.95× (32 s) | 0.96× (55 s) | 0.97× (0.623s) |
Rust4 | Compiled | 0.85× (29 s) | 0.86× (49 s) | 0.66× (0.425 s) |
C++14 | Compiled | 1.06× (36 s) | 1.07× (61 s) | 0.95× (0.608 s) |
Haskell5 | Compiled | 1.06× (36 s) | 1.05× (60 s) | 1.78× (1.141 s) |
Java6 | JIT | 1.15× (39 s) | 1.16× (66 s) | 1.28× (0.818 s) |
Node.js7 | JIT | 1.24× (42 s) | 1.30× (74 s) | 1.67× (1.071 s) |
Mono C# | JIT | 1.50× (51 s) | 1.53× (87.4 s) | 2.13× (1.363 s) |
Cython8 | Compiled | 1.97× (67 s) | 1.96× (112 s) | 1.19× (0.765 s) |
Lua9 | Interpreted | 6.41× (218 s) | 6.31× (360 s) | 28.6× (18.304 s) |
Cython | Compiled | 22.0× (751 s) | 22.1× (1262 s) | 55.8× (35.742 s) |
Perl | Interpreted | 26.8× (910 s) | 20.3× (1156 s) | 59.6× (38.214 s) |
Ruby10 | Interpreted | 34.2× (1162 s) | 31.0× (1770 s) | 46.0× (29.454 s) |
Python | Interpreted | 57.0× (1937 s) | 58.2× (3318 s) | 98.3× (63.032 s) |
The second column in Table 5-1 indicates the language type, where compiled refers to natively compiled languages, JIT refers to just-in-time compiled languages, and interpreted refers to code that is executed by interpreters. The distinction in these language types is described in detail throughout this chapter and is not quite as clear-cut as presented in the table.
All of the programs use between 98 percent and 99 percent of the CPU while executing. The relative performance of Java, Node.js, and Mono C# is impressive given that code is compiled dynamically (“just-in-time”), which is discussed later in this chapter. Any dynamic compilation latency is included in the timings, as the test script includes the following Bash script code to calculate the execution duration of each program:
Duration="5000000"
echo -e " The C++ Code Example"
T="$(date +%s%N)"
./n-body $Duration
T="$(($(date +%s%N)-T))"
T=$((T/1000000))
echo "It took ${T} milliseconds to run the C++ test"
The C++14 code is the version of the C++ programming language that was published in 2014 (needs gcc 5+ for full support). This is discussed again later in this chapter. The program contains optimizations that are specific to this release of C++, and interestingly, while this version performs better on the desktop computer, it slightly underperforms on the PocketBeagle. The Java program uses the +AggressiveOpts
flag to enable performance optimization, and it was used because it did not involve modifying the source code.
The results for Python are particularly poor because of the algorithmic nature of the problem. However, the benchmarks (benchmarksgame.alioth.debian.org
) indicate that the range will be 9 to 100 times slower than the optimized C++ code for general processing to algorithm-rich code, respectively. If you are comfortable with Python and you would like to improve upon its performance, then you can investigate Cython, which is a Python compiler that automatically removes the dynamic typing capability and enables you to generate C code directly from your Python code. Cython and the extension of Python with C/C++ are discussed at the end of this chapter.
The final column provides the results for the same code running on a desktop computer virtual machine. You can see that the relative performance of the applications is broadly in line, but also note that the C++ program runs 40 times faster on the single i7 thread than it does on the PocketBeagle at 1 GHz. I hope that will help you frame your expectations with respect to the type of numerical processing that is possible on a standard Beagle board, particularly when investigating computationally expensive applications such as signal processing and computer vision.
As previously discussed, this is only one numerically oriented benchmark test, but it is somewhat indicative of the type of performance you should expect from each language. There have been many studies on the performance of languages; however, a well-specified analysis by Hundt (2011) has found that in terms of performance, “C++ wins out by a large margin. However, it also required the most extensive tuning efforts, many of which were done at a level of sophistication that would not be available to the average programmer.”
In the previous section, the clock frequency of the Beagle board was adjusted dynamically at run time. The BeagleBoard.org Debian image has various governors that can be used to profile the performance/power usage ratio. For example, if you were building a battery-powered PocketBeagle application that has low processing requirements, you could reduce the clock frequency to conserve power. You can find out information about the current state of the board by typing the following:
debian@ebb:~$ sudo apt install cpufrequtils
debian@ebb:~$ cpufreq-info
…
available cpufreq governors: conservative, ondemand, userspace,
powersave, performance, schedutil
current policy: frequency should be within 300 MHz and 1000 MHz.
The governor "performance" may decide which speed to use
within this range.
current CPU frequency is 1000 MHz.
cpufreq stats: 300 MHz:0.00%, 600 MHz:0.00%, 720 MHz:0.00%,
800 MHz:0.00%, 1000 MHz:100.00%
You can see that different governors are available, with the profile names conservative
, ondemand
, userspace
, powersave
, performance
, and schedutil
. To enable one of these governors, type the following:
debian@ebb:~$ sudo cpufreq-set -g ondemand
debian@ebb:~$ cpufreq-info
… The governor "ondemand" may decide which speed to use within this range.
debian@ebb:~$ sudo cpufreq-set -f 600MHz
debian@ebb:~$ cpufreq-info
… current CPU frequency is 600 MHz.
The ondemand
is useful as it dynamically switches the CPU frequency. For example, if the CPU frequency is currently 600 MHz and the average CPU usage between governor samplings is above the threshold (called the up_threshold
), then the CPU frequency will be automatically increased. You can tweak these and other settings using their sysfs entries. For example, to set the threshold at which the CPU frequency rises to the point at which the CPU load reaches 90 percent of available capacity, use the following:
debian@ebb:~$ sudo cpufreq-set -g ondemand
debian@ebb:~$ cd /sys/devices/system/cpu/cpufreq/ondemand/
debian@ebb:/sys/devices/system/cpu/cpufreq/ondemand$ ls
ignore_nice_load min_sampling_rate sampling_down_factor up_threshold
io_is_busy powersave_bias sampling_rate
debian@ebb: … /ondemand$ cat up_threshold
95
debian@ebb: … /ondemand$ sudo sh -c "echo 90 > up_threshold"
debian@ebb: … /ondemand$ cat up_threshold
90
If these tools are not installed on your board, you must install the cpufrequtils
package. On Debian Stretch, the default governor is performance
, but you can switch to ondemand
by editing the cpufrequtils
file in /etc/init.d/
as follows:
debian@ebb:~$ cd /etc/init.d
debian@ebb:/etc/init.d$ more cpufrequtils | grep GOVERNOR=
GOVERNOR="performance"
debian@ebb:/etc/init.d$ sudo nano cpufrequtils
debian@ebb:/etc/init.d$ more cpufrequtils | grep GOVERNOR=
GOVERNOR="ondemand"
debian@ebb:/etc/init.d$ sudo reboot
A scripting language is a computer programming language that is used to specify script files, which are interpreted directly by a run-time environment to perform tasks. Many scripting languages are available, such as Bash, Perl, Lua, and Python, and these can be used to automate the execution of tasks on a Beagle board, such as system administration, interaction, and even interfacing to electronic components.
Which scripting language should you choose for a Beagle board? There are many strong opinions, and it is a difficult topic, as Linux users tend to have a favorite scripting language; however, you should choose the scripting language with features that suit the task at hand. Here are some examples:
These four scripting languages are available for the Beagle standard Debian image. It would be useful to have some knowledge of all of these scripting languages, as you may find third-party tools or libraries that make your current project straightforward. This section provides a brief overview of each of these languages, including a concise segment of code that performs a similar function in each language. It finishes with a discussion about the advantages and disadvantages of scripting languages in general.
In Chapter 2 an approach is described for changing the state of the on-board LEDs using Linux shell commands. It is possible to turn an LED on or off, and even make it flash. For example, you can use the following:
root@ebb:/sys/class/leds/beaglebone:green:usr3# echo none > trigger
root@ebb:/sys/class/leds/beaglebone:green:usr3# echo 1 > brightness
root@ebb:/sys/class/leds/beaglebone:green:usr3# echo 0 > brightness
to turn a user LED on and off. This section examines how it is possible to do the same tasks but in a structured programmatic form.
Bash scripts are a great choice for short scripts that do not require advanced programming structures, and that is exactly the application to be developed here. The first program leverages the Linux console commands such as echo
and cat
to create the concise script in Listing 5-1 that enables you to choose, using command-line arguments, whether you want to turn the USR3 LED on or off or place it in a flashing mode. For example, using this script by calling ./bashLED on
would turn the USR3 LED on. It also provides you with the trigger status information.
The script is available in the directory /chp05/bashLED/
. If you entered the script manually using the nano
editor, then the file needs to have the executable flag set before it can be executed (the git repository retains executable flags). Therefore, to allow all users to execute this script, use the following:
/chp05/bashLED$ chmod ugo+x bashLED
What is happening within this script? First, all of these command scripts begin with a sha-bang #!
followed by the name and location of the interpreter to be used, so #!/bin/bash
in this case. The file is just a regular text file, but the sha-bang is a magic-number code to inform the OS that the file is an executable. Next, the script defines the path to the LED for which you want to change state using the variable LED3_PATH
. This allows the default value to be easily altered if you want to use a different user LED or path.
The script contains a function called removeTrigger
, mainly to demonstrate how functions are structured within Bash scripting. This function is called later in the script. Each if
is terminated by a fi
. The ;
after the if
statement terminates that statement and allows the statement then
to be placed on the same line. The elif
keyword means else if
, which allows you to have multiple comparisons within the one if
block. The newline character
terminates statements.
The first if
statement confirms that the number of arguments passed to the script ($#
) is not equal to 1
. Remember that the correct way to call this script is of the form ./bashLED on
. Therefore, on
will be the first user argument that is passed ($1
), and there will be one argument in total. If there are no arguments, then the correct usage will be displayed, and the script will exit with the return code 2
. This value is consistent with Linux system commands, where an exit value of 2 indicates incorrect usage. Success is indicated by a return value of 0
, so any other nonzero return value generally indicates the failure of a script.
If the argument that was passed is on
, then the code displays a message; calls the removeTrigger
function; and writes the string "1"
to the brightness
file in the LED3 /sys/
directory. The remaining functions modify the USR3 LED values in the same way as described in Chapter 2. You can execute the script as follows:
debian@ebb:~/exploringbb/chp05/bashLED$ ./bashLED
There are no arguments. Usage is:
bashLED Command where command is one of
on, off, flash or status e.g. bashLED on
debian@ebb:~/exploringbb/chp05/bashLED$ ./bashLED status
The LED Command that was passed is: status
none … mmc0 [timer] oneshot disk-activity …
debian@ebb:~/exploringbb/chp05/bashLED$ ./bashLED on
The LED Command that was passed is: on
Turning the LED on
debian@ebb:~/exploringbb/chp05/bashLED$ sudo ./bashLED flash
debian@ebb:~/exploringbb/chp05/bashLED$ ./bashLED off
Notice that the script was prefixed by sudo
when it was called for the flash function only. This is because the flash function enables the timer state on the LED, which creates two new file entries in the directory called delay_on
and delay_off
. Unlike the other entries in this directory, these two new entries are not in the gpio
group, and therefore the user debian does not have permission to write to them.
debian@ebb:/sys/class/leds/beaglebone:green:usr3$ ls -l
-rw-rw-r-- 1 root gpio 4096 May 13 09:16 brightness
-rw-r--r-- 1 root root 4096 May 13 17:42 delay_off
-rw-r--r-- 1 root root 4096 May 13 17:42 delay_on …
For security reasons, you cannot use the setuid bit on a script to set it to execute as root. If users had write access to this script and its setuid bit was set as root, then they could inject any command that they wanted into the script and would have de facto superuser access to the system. For a comprehensive online guide to Bash scripting, please see Mendel Cooper's “Advanced Bash-Scripting Guide” at www.tldp.org/LDP/abs/html/
.
Lua is the best performing interpreted language in Table 5-1 by a significant margin. In addition to good performance, Lua has a clean and straightforward syntax that is accessible for beginners. The interpreter for Lua has a small footprint—approximately 131 KB in size (ls -lh /usr/bin/lua5.3
), which makes it suitable for low-footprint embedded applications. For example, Lua can be used successfully on the ultra-low-cost ($2–$5) ESP Wi-Fi modules that are described in Chapter 12, despite their modest memory allocations. In fact, once a platform has an ANSI C compiler, then the Lua interpreter can be built for it. However, one downside is that the standard library of functions is somewhat limited in comparison to other more general scripting languages, such as Python. You can install the Lua interpreter using sudo apt install lua5.3
.
Listing 5-2 provides a Lua script that has the same structure as the Bash script, so it is not necessary to discuss it in detail.
You can execute this script in the same manner as the bashLED
script (e.g., ./luaLED.lua on
or by typing lua luaLED.lua on
from the /chp05/luaLED/
directory), and it will result in a comparable output. There are two things to be careful of with Lua in particular: strings are indexed from 1, not 0; and functions can return multiple values, unlike most languages. Lua has a straightforward interface to C/C++, which means that you can execute compiled C/C++ code from within Lua or use Lua as an interpreter module within your C/C++ programs. There is an excellent reference manual at www.lua.org/manual/
and a six-page summary of Lua at tiny.cc/beagle501
.
Perl is a feature-rich scripting language that provides you with access to a huge library of reusable modules and portability to other OSs (including Windows). Perl is best known for its text processing and regular expressions modules. In the late 1990s it was a popular language for server-side scripting for the dynamic generation of web pages. Later it was superseded by technologies such as Java servlets, Java Server Pages (JSP), and PHP. The language has evolved since its birth in the 1980s and now includes support for the OOP paradigm. Perl 5 (v5.24+) is installed by default on the Debian Linux image. Listing 5-3 provides a segment of a Perl example that has the same structure as the Bash script, so it is not necessary to discuss it in detail. Apart from the syntax, little has actually changed in the translation to Perl.
A few small points are worth noting in the code. The <
or >
sign on the filename indicates whether the file is being opened for read or write access, respectively; the arguments are passed as $ARGV[0…n]
, and the number of arguments is available as the value $#ARGV
. A file open, followed by a read or write, and a file close are necessary to write the values to /sys/
; and the arguments are passed to the subroutine writeLED3
and these are received as the values $_[0]
and $_[1]
, which is not the most beautiful programming syntax, but it works perfectly well. To execute this code, simply type sudo ./perlLED.pl on
, as the sha-bang identifies the Perl interpreter. You could also execute it by typing perl perlLED.pl status
.
For a good resource about getting started with installing and using Perl 5, see the guide “Learning Perl” at learn.perl.org
.
Python is a dynamic and strongly typed OOP language that was designed to be easy to learn and understand. Dynamic typing means you do not need to associate a type (e.g., integer, character, string) with a variable; rather, the value of the variable “remembers” its own type. Therefore, if you were to create a variable x=5
, the variable x
would behave as an integer; but if you subsequently assign it using x="test"
, it would then behave like a string. Statically typed languages such as C/C++ or Java would not allow the redefinition of a variable in this way (within the same scope). Strongly typed means that the conversion of a variable from one type to another requires an explicit conversion. The advantages of object-oriented programming structures are discussed later in this chapter. Python is installed by default on the BeagleBoard Debian image. The Python example to flash the LED is provided in Listing 5-4.
The formatting of this code is important—in fact, Python enforces the layout of your code by making indentation a structural element. For example, after the line if len(sys.argv)!=2:
, the next few lines are “tabbed” in. If you did not tab in one of the lines—for example, the sys.exit(2)
line—then it would not be part of the conditional if
statement, and the code would always exit at this point in the program. To execute this example, in the pythonLED
directory, enter the following:
debian@ebb:~/exploringbb/chp05/pythonLED$ sudo ./pythonLED.py flash
Flashing the LED
debian@ebb:~/exploringbb/chp05/pythonLED$ ./pythonLED.py status
… [timer] oneshot disk-activity ide-disk mtd nand-disk heartbeat …
Python is popular on the Beagle platform for good pedagogical reasons, but as users turn their attention to more advanced applications, it is difficult to justify the performance deficit. This chapter concludes with a discussion on how you can use either Cython or combine Python with C/C++ to dramatically improve the performance of Python. However, the complexity of Cython itself should motivate you to consider using C/C++ directly.
To conclude this discussion of scripting, there are several strong choices for applications on the Beagle platform. Table 5-2 lists some of the key advantages and disadvantages of command scripting, when considered in the context of the compiled languages discussed shortly.
Table 5-2: Advantages and Disadvantages of Command Scripting
ADVANTAGES | DISADVANTAGES |
Perfect for automating Linux system administration tasks that require calls to Linux commands. | Performance is poor for complex numerical or algorithmic tasks. |
Easy to modify and adapt to changes. Source code is always present and complex toolchains (see Chapter 7) are not required to make modifications. Generally, nano is the only tool that you need. |
Generally, relatively poor/slow programming support for data structures, graphical user interfaces, sockets, threads, etc. |
Generally, straightforward programming syntax and structure that are reasonably easy to learn when compared to languages like C++ and Java. | Generally, poor support for complex applications involving multiple, user-developed modules or components (Python and Perl do support OOP). |
Generally, quick turnaround in coding solutions by occasional programmers. | Code is in the open. Direct access to view your code can be an intellectual property or a security concern. |
Lack of development tools (e.g., refactoring). |
With the interpreted languages just discussed, the source code text file is “executed” by the user passing it to a run-time interpreter, which then translates or executes each line of code. JavaScript and Java have different life cycles and are quite distinct languages.
As discussed in Chapter 2, in the section “Node.js, Cloud9, and BoneScript,” Node.js is JavaScript that is run on the server side. JavaScript is an interpreted language by design; however, thanks to the V8 engine that was developed by Google for its Chrome web browser, Node.js actually compiles JavaScript into native machine instructions as it is loaded by the engine. This is called just-in-time (JIT) compilation or dynamic translation. As demonstrated at the beginning of this chapter, Node.js's performance for the numerical computation tasks is impressive for an interpreted language because of optimizations for the ARMv7 platform.
Listing 5-5 is the same LED code example written using JavaScript and executed by calling the nodejs
executable.
The code is available in the /chp05/nodejsLED
/ directory, and it can be executed by typing nodejs nodejsLED.js [option]
. The code has been structured in the same way as the previous examples, and there are not too many syntactical differences; however, there is one major difference between Node.js and other languages: functions are called asynchronously. Up to this point, all the languages discussed followed a sequential-execution mode. Therefore, when a function is called, the program counter (also known as the instruction pointer) enters that function and does not reemerge until the function is complete. Consider, for example, code like this:
functionA();
functionB();
The functionA()
is called, and functionB()
will not be called until functionA()
is fully complete. This is not the case in Node.js! In Node.js, functionA()
is called first, and then Node.js continues executing the subsequent code, including entering functionB()
, while the code in functionA()
is still being executed. This presents a serious difficulty for the current application, with this segment of code in particular:
case 'flash':
console.log("Making the LED Flash");
writeLED("/trigger", "timer", LED3_PATH);
writeLED("/delay_on", "50", LED3_PATH);
writeLED("/delay_off", "50", LED3_PATH);
break;
The first call to writeLED()
sets up the sysfs
file system (as described in Chapter 2) to now contain new delay_on
and delay_off
file entries. However, because of the asynchronous nature of the calls, the first writeLED()
call has not finished setting up the file system before the next two writeLED()
calls are performed. This means that the delay_on
and delay_off
file system entries are not found, and the code to write to them fails. You should test this by changing the call near the top of the program from fs.writeFileSync(…)
to fs.writeFile(…)
.
To combat this issue you can synchronize (prevent threads from being interrupted) the block of code where the three writeLED()
functions are called, ensuring that the functions are called sequentially. Alternatively, as shown in this code example, you can use a special version of the Node.js writeFile()
function called writeFileSync()
to ensure that the first function call to modify the file system blocks the other writeFileSync()
calls from taking place.
Node.js allows asynchronous calls because they help ensure that your code is “lively.” For example, if you performed a database query, your code may be able to do something else useful while awaiting the result. When the result is available, a callback function is executed in order to process the received data. This asynchronous structure is perfect for Internet-attached applications, where posts and requests are being made of websites and web services and it is not clear when a response will be received (if at all). Node.js has an event loop that manages all the asynchronous calls, creating threads for each call as required and ensuring that the callback functions are executed when an asynchronous call completes its assigned tasks. Node.js is revisited in Chapter 11 when the Internet of Things is discussed.
Up to this point in the chapter, interpreted languages are examined, meaning the source code file (a text file) is executed using an interpreter or dynamic translator at run time. Importantly, the code exists in source code form, right up to the point when it is executed using the interpreter.
With traditional compiled languages, the source code (a text file) is translated directly into machine code for a particular platform using a set of tools, which we will call a compiler for the moment. The translation happens when the code is being developed; once compiled, the code can be executed without needing any additional run-time tools.
Java is a hybrid language: you write your Java code in a source file, e.g., example.java
, which is a regular text file. The Java compiler (javac
) compiles and translates this source code into machine code instructions (called bytecodes) for a Java virtual machine (VM). Regular compiled code is not portable between hardware architectures, but bytecode files (.class
files) can be executed on any platform that has an implementation of the Java VM. Originally, the Java VM interpreted the bytecode files at run time; however, more recently, dynamic translation is employed by the VM to convert the bytecodes into native machine instructions at run time.
The key advantage of this life cycle is that the compiled bytecode is portable between platforms, and because it is compiled to a generic machine instruction code, the dynamic translation to “real” machine code is efficient. The downside of this structure when compared to compiled languages is that the VM adds overhead to the execution of the final executable.
The Java Runtime Environment (JRE), which provides the Java virtual machine (JVM), is not installed on the Beagle Debian image by default because it occupies approximately 177 MB when installed.
Listing 5-6 provides a source code example that is also available in the GitHub repository in bytecode form.
The program can be executed using the run
script that is in the /chp05/javaLED/
directory. You can see that the class is placed in the package directory exploringBB
.
Early versions of Java suffered from poor computational performance; however, more recent versions take advantage of dynamic translation at run time (just-in-time, or JIT, compilation) and, as demonstrated at the start of this chapter, the performance was less than 15 percent slower (including dynamic translation) than that of the natively compiled C++ code, with only a minor additional memory overhead. Table 5-3 lists some of the advantages and disadvantages of using Java for development on the Beagle platform.
Table 5-3: Advantages and Disadvantages of Java on the Beagle Platform
ADVANTAGES | DISADVANTAGES |
Code is portable. Code compiled on the PC can be executed on any Beagle board or another embedded Linux platform. | Sandboxed applications do not have access to system memory, registers, or system calls (except through /proc/ ) or Java Native Interface (JNI). |
There is a vast and extensive library of code available that can be fully integrated in your project. | Executing as root is slightly difficult because of required environment variables. |
Full OOP support. | It is not suitable for scripting. |
Can be used for user-interface application development on a Beagle board that is attached to a display. | Computational performance is respectable but slower than optimized C/C++ programs. Slightly heavier on memory. |
Strong support for multithreading. | Strictly typed and no unsigned integer types. |
Has automatic memory allocation and de-allocation using a garbage collector, removing memory leak concerns. | Royalty payment is required if deployed to a platform that “involves or controls hardware.”11 |
To execute a Java application under Debian, where it needs access to the /sys/
directory, you need the application to run with root access. Unfortunately, because you need to pass the bytecode (.class
) file to the Java VM, you must call sudo
and create a temporary shell of the form sudo sh -c 'java myClass'
. The application can be executed using the following:
…/chp05/javaLED$ sudo sh -c 'java exploringBB.LEDExample Off'
Starting the LED Java Application
Turning the LED Off
…/chp05/javaLED$ sudo sh -c 'java exploringBB.LEDExample On'
Starting the LED Java Application
Turning the LED On
C++ was developed by Bjarne Stroustrup at Bell Labs (now AT&T Labs) during 1983–1985. It is based on the C language (named in 1972) that was developed at AT&T for UNIX systems in the early 1970s (1969–1973) by Dennis Ritchie. As well as adding an object-oriented (OO) framework (originally called “C with Classes”), C++ also improves the C language by adding features such as better type checking. It quickly gained widespread usage, which was largely due to its similarity to the C programming language syntax and the fact that it allowed existing C code to be used when possible. C++ is not a pure OO language but rather a hybrid, having the organizational structure of OO languages but retaining the efficiencies of C, such as typed variables and pointers.
Unlike Java, C++ is not “owned” by a single company. In 1998 the International Organization for Standardization (ISO) committee adopted a worldwide uniform language specification that aimed to remove inconsistencies between the various C++ compilers (Stroustrup, 1998). This standardization continues today with C++11 approved by the ISO in 2011 (gcc 4.7+ supports the flag -std=c++11
), C++14 fully supported by gcc version 5, and many features of C++17 supported in gcc version 6 (with more to come in gcc version 7). At the time of writing, the current version of gcc and exact set of features available can be determined using this:
debian@ebb:~/exploringbb/chp05/overview$ g++ -v
…
gcc version 6.3.0 20170516 (Debian 6.3.0-18+deb9u1)
debian@ebb:~/exploringbb/chp05/overview$ more version.cpp
#include <iostream>
int main(int argc, char** argv) {
std::cout << __cplusplus << std::endl;
return 0;
}
…/chp05/overview$ g++ version.cpp -o version
…/chp05/overview$ ./version
201402
…/chp05/overview$ g++ -std=c++17 version.cpp -o version
…/chp05/overview$ ./version
201500
While it is important to understand the functionality available to you through your version of gcc, many of the newer features of C++ are not vital to developing code on embedded devices.
Why am I covering C and C++ in more detail than other languages in this book?
Table 5-4 lists some advantages and disadvantages of using C/C++ on the Beagle boards. The next section reviews some of the fundamentals of C and C++ programming to ensure that you have the skills necessary for the remaining chapters in this book. It is not possible to cover every aspect of C and C++ programming in just one chapter of one book. The “Further Reading” section at the end of this chapter directs you to recommended texts.
Table 5-4: Advantages and Disadvantages of C/C++ on the Beagle Boards
ADVANTAGES | DISADVANTAGES |
You can build code directly on the board or you can cross-compile code. The C/C++ languages are ISO standards, not owned by a single vendor. | Compiled code is not portable. Code compiled for your x86 desktop will not run on the Beagle board's ARM processor. |
C++ has full support for procedural programming, OOP, and support for generics through the use of STL (Standard Template Library). | Many consider the languages to be complex to master. There is a tendency to need to know everything before you can do anything. |
It gives excellent computational performance, especially if optimized; however, optimization can be difficult and can reduce the portability of your code. | The use of pointers and the low-level control available makes code prone to memory leaks. With careful coding these can be avoided and can lead to efficiencies over dynamic memory management schemes. |
Can be used for user-interface application development on the Beagle boards using third-party libraries. Libraries such as Qt and Boost provide extensive additional resources for components, networking, etc. | By default, C and C++ do not support graphical user interfaces, network sockets, etc. Third-party libraries are required. |
Offers low-level access to glibc for integrating with the Linux system. Programs can be setuid to root. |
Not suitable for scripting (there is a C shell, csh , that does have syntax like C). Not ideal for web development either. |
The Linux kernel is written in C and having knowledge of C/C++ can help if you ever need to write device drivers or contribute to Linux kernel development. | C++ attempts to span from low-level to high-level programming tasks, but it can be difficult to write very scalable enterprise or web applications. |
The next section provides a revision of the core principles that have been applied to examples on the Beagle boards. It is intended to serve as an overview and a set of reference examples that you can come back to again and again. It also focuses on topics that cause my students difficulties, pointing out common mistakes. Also, please remember that course notes for my Object-Oriented Programming module are publicly available at ee402.eeng.dcu.ie
along with further support materials.
The following examples can be edited using the nano editor and compiled on the Beagle boards directly using the gcc and g++ compilers, which are installed by default. The code is in the directory chp05/overview/
.
The first example you should always write in any new language is “Hello World.” Listings 5-7 and 5-8 provide C and C++ code, respectively, for the purpose of a direct comparison of the two languages.
The #include
call is a preprocessor directive that effectively loads the contents of the stdio.h
file (/usr/include/stdio.h
) in the C case, and the iostream
header (/usr/include/c++/6/iostream
) file in the C++ case, and copies and pastes the code in at this exact point in your source code file. These header files contain the function prototypes, enabling the compiler to link to and understand the format of functions such as printf()
in stdio.h
and streams like cout
in iostream
. The actual implementation of these functions is in shared library dependencies. The angular brackets (< >) around the include filename means that it is a standard, rather than a user-defined include
(which would use double quotes).
The main()
function is the starting point of your application code. There can be only one function called main()
in your application. The int
in front of main()
indicates that the program will return a number to the shell prompt. As stated, it is good to use 0
for successful completion, 2
for invalid usage, and any other set of numbers to indicate failure conditions. This value is returned to the shell prompt using the line return 0
in this case. The main()
function will return 0
by default. Remember that you can use echo $?
at the shell prompt to see the last value that was returned.
The parameters of the main()
function are int argc
and char *argv[]
. As you saw in the scripting examples, the shell can pass arguments to your application, providing the number of arguments (argc
) and an array of strings (*argv[]
). In C/C++ the first argument passed is argv[0]
, and it contains the name and full path used to execute the application.
The C code line printf("Hello World!
");
allows you to write to the Linux shell, with the
representing a new line. The printf()
function provides you with additional formatting instructions for outputting numbers, strings, etc. Note that every statement is terminated by a semicolon.
The C++ code line std::cout << "Hello World!" << std::endl;
outputs a string just like the printf()
function. In this case, cout
represents the output stream, and the function used is actually the <<
, which is called the output stream operator. The syntax is discussed later, but std::cout
means the output stream in the namespace std
. The endl
(end line) representation is the same as
. This may seem more verbose, but you will see why it is useful later in the discussion on C++ classes. These programs can be compiled and executed directly on the Beagle board by typing the following:
…/chp05/overview$ gcc helloworld.c -o helloworldc
…/chp05/overview$ ./helloworldc
Hello World!
…/chp05/overview$ g++ helloworld.cpp -o helloworldcpp
…/chp05/overview$ ./helloworldcpp
Hello World!
The sizes of the C and C++ executables are different to account for the different header files, output functions, and exact compilers that are used.
debian@ebb:~/exploringbb/chp05/overview$ ls -l helloworldc*
-rwxr-xr-x 1 debian debian 8348 May 14 02:48 helloworldc
-rwxr-xr-x 1 debian debian 9152 May 14 02:48 helloworldcpp
You just saw how to build a C or C++ application, but there are a few intermediate steps that are not obvious in the preceding example, as the intermediate stage outputs are not retained by default. Figure 5-1 illustrates the full build process from preprocessing right through to linking.
You can perform the steps in Figure 5-1 yourself. Here is an example of the actual steps that were performed using the Helloworld.cpp
code example. The steps can be performed explicitly as follows, so that you can view the output at each stage:
debian@ebb:/tmp$ ls -l helloworld.cpp
-rw-r--r-- 1 debian debian 114 May 14 02:51 helloworld.cpp
debian@ebb:/tmp$ g++ -E helloworld.cpp > processed.cpp
debian@ebb:/tmp$ ls -l processed.cpp
-rw-r--r-- 1 debian debian 641377 May 14 02:51 processed.cpp
debian@ebb:/tmp$ g++ -S processed.cpp -o helloworld.s
debian@ebb:/tmp$ ls -l helloworld.s
-rw-r--r-- 1 debian debian 3161 May 14 02:52 helloworld.s
debian@ebb:/tmp$ g++ -c helloworld.s
debian@ebb:/tmp$ ls
helloworld.cpp helloworld.o helloworld.s processed.cpp
debian@ebb:/tmp$ g++ helloworld.o -o helloworld
debian@ebb:/tmp$ ls -l helloworld
-rwxr-xr-x 1 debian debian 9148 May 14 02:53 helloworld
debian@ebb:/tmp$ ./helloworld
Hello World!
You can see the text format output after preprocessing by typing less processed.cpp
, where you will see the necessary header files pasted in at the top of your code. At the bottom of the file you will find your code. This file is passed to the C/C++ compiler, which validates the code and generates platform-independent assembler code (.s
). You can view this code by typing less helloworld.s
, as illustrated in Figure 5-1.
This .s
text file is then passed to the assembler, which converts the platform-independent instructions into binary instructions for the Beagle board (the .o
file). You can see the assembly language code that was generated if you use the objdump
(object file dump) tool on your board by typing objdump -D helloworld.o
, as illustrated in Figure 5-1.
Object files contain generalized binary assembly code that does not yet provide enough information to be executed on the board. However, after linking the final executable code, helloworld
contains the target-specific assembly language code that has been combined with the libraries, statically and dynamically as required—you can use the objdump
tool again on the executable, which results in the following output:
…/chp05/overview$ objdump -d helloworldcpp | less
helloworldcpp: file format elf32-littlearm
Disassembly of section .init:
00000668 <_init>:
668: e92d4008 push {r3, lr}
66c: eb00002f bl 730 <call_weak_fn>
670: e8bd8008 pop {r3, pc}
…
The first column is the memory address, which steps by 4 bytes (32-bits) between each instruction (i.e., 66c
− 668
= 4)
. The second column is the full 4-byte instruction at that address. The third and fourth columns are the human-readable version of the second column that describes the opcode and operand of the 4-byte instruction. For example, the first instruction at address 668
is a push
, which pushes r3
, which is one of the ARM processor's sixteen 32-bit registers (labeled r0-r15
), followed by lr
(the link register, r14
) onto the stack.
Understanding ARM instructions is another book in and of itself (see infocenter.arm.com
). However, it is useful to appreciate that any natively compiled code, whether it uses the OOP paradigm or not, results in low-level machine code, which does not support dynamic typing, OOP, or any such high-level structures. In fact, whether you use an interpreted or compiled language, the code must eventually be converted to machine code so that it can execute on the board's ARM processor.
Is the HelloWorld
example the shortest program that can be written in C or C++? No, Listing 5-9 is the shortest valid C and C++ program.
This is a fully functional C and C++ program that compiles with no errors and works perfectly, albeit with no output. Therefore, in building a C/C++ program, there is no need for libraries; there is no need to specify a return type for main()
, as it defaults to int
; the main()
function returns 0
by default in C++ and an undefined number in C (see the following echo $?
call); and an empty function is a valid function. This program will compile as a C or C++ program as follows:
…/chp05/overview$ gcc short.c -o shortc
short.c:1:1: warning: return type defaults to 'int' …
…/chp05/overview$ g++ short.c -o shortcpp
…/chp05/overview$ ls -l shortc*
-rwxr-xr-x 1 debian debian 8276 May 14 03:10 shortc
-rwxr-xr-x 1 debian debian 8292 May 14 03:10 shortcpp
…/chp05/overview$ ./shortc
…/chp05/overview$ echo $?
0
…/chp05/overview$ ./shortcpp
…/chp05/overview$ echo $?
0
This is one of the greatest weaknesses of C and C++. There is an assumption that you know everything about the way the language works before you write anything. In fact, aspects of the preceding example might be used by programmers to demonstrate how clever they are, but they are actually demonstrating poor practice in making their code unreadable by less “expert” programmers. For example, if you rewrite the C++ code in short.cpp
to include comments and explicit statements, to create short2.cpp
, and then compile both using the -O3
optimization flag, the output will be as follows:
…/chp05/overview$ more short.cpp
main(){}
…/chp05/overview$ more short2.cpp
// A really useless program, but a program nevertheless
int main(int argc, char *argv[]){
return 0;
}
…/chp05/overview$ g++ -O3 short.cpp -o short01
…/chp05/overview$ g++ -O3 short2.cpp -o short02
…/chp05/overview$ ls -l short0*
-rwxr-xr-x 1 debian debian 8292 May 14 03:15 short01
-rwxr-xr-x 1 debian debian 8292 May 14 03:15 short02
Note that the executable size is exactly the same! Adding the comment, the explicit return statement, the explicit return type, and explicit arguments has had no impact on the size of the final binary application. However, the benefit is that the actual functionality of the code is much more readily understood by a novice programmer.
You can build with the flag -static
to statically link the libraries, rather than the default form of linking dynamically with shared libraries. This means that the compiler and linker effectively place all the library routines required by your code directly within the program executable.
…/chp05/overview$ g++ -O3 short.cpp -static -o short_static
…/chp05/overview$ ls -l short_static
-rwxr-xr-x 1 debian debian 448792 May 14 03:19 short_static
It is clear that the program executable size has grown significantly from 8 KB to 449 KB. One advantage of this form is that the program can be executed by ARM systems on which the C++ standard libraries are not installed.
With dynamic linking, it is useful to note that you can discover which shared library dependencies your compiled code is using, by calling ldd
.
…/chp05/overview$ ldd shortcpp
linux-vdso.so.1 (0xbefcd000)
libstdc++.so.6 => /usr/lib/arm-linux-gnueabihf/libstdc++.so.6 (0xb6dcf000)
libm.so.6 => /lib/arm-linux-gnueabihf/libm.so.6 (0xb6d57000)
libgcc_s.so.1 => /lib/arm-linux-gnueabihf/libgcc_s.so.1 (0xb6d2e000)
libc.so.6 => /lib/arm-linux-gnueabihf/libc.so.6 (0xb6c40000)
/lib/ld-linux-armhf.so.3 (0xb6eed000)
You can see from this output that the g++ compiler (and glibc) on the Debian image for the Beagle boards has been patched to support the generation of hard floating-point (gnueabihf
) instructions by default. This allows for faster code execution with floating-point numbers than if it used the soft floating-point ABI (application binary interface) to emulate floating-point support in software (gnueabi
).
A variable is a data item stored in a block of memory that has been reserved for it. The type of the variable defines the amount of memory reserved and how it should behave (see Figure 5-2). This figure describes the output of the code example sizeofvariables.c
in Listing 5-10.
Listing 5-10 details various variables available in C/C++. When you create a local variable c
, it is allocated a box/block of memory on the stack (predetermined reserved fast memory) depending on its type. In this case, c
is an int
value; therefore, four bytes (32 bits) of memory are allocated to store the value. Assume that variables in C/C++ are initialized with arbitrary values; therefore, in this case c = 545;
replaces that initial random value by placing the number 545
in the box. It does not matter if you store the number 0
or 2,147,483,647
in this box: it will still occupy 32 bits of memory! Please note that there is no guarantee regarding the ordering of local variable memory—it was fortuitously linear in this particular example.
The sizeof(c)
operator returns the size of the type of the variable in bytes. In this example, it will return 4
for the size of the int
type. The &c
call can be read as the “address of” c
. This provides the address of the first byte that stores the variable c
, in this case returning 0xbe8d6688
. The %.4f
on the first line means display the floating-point number to four decimal places. Executing this program on a PocketBeagle gives the following:
/chp05/overview$ ./sizeofvariables
a val 3.1416 & size 8 bytes (@addr 0xbe8d6690).
b val 25.00 & size 4 bytes (@addr 0xbe8d668c).
c val 545 (oct 1041, hex 221) & size 4 bytes (@addr 0xbe8d6688).
d val 123 & size 4 bytes (@addr 0xbe8d6684).
e val A & size 1 bytes (@addr 0xbe8d6683).
f val 1 and size 1 bytes (@addr 0xbe8d6682).
The Beagle boards have 32-bit microprocessors, so you are using four bytes to represent the int
type. The smallest unit of memory that you can allocate is one byte, so, yes, you are representing a Boolean value with one byte, which could actually store eight unique Boolean values. You can operate directly on variables using operators. The program operators.c
in Listing 5-11 contains some points that often cause difficulty in C/C++:
This will give the following output:
/chp05/overview$ ./operators
The value of c=2 and a=2.
The value of d=2 and b=3.
The value of f=10.00 and e=9.
The value of g=65 and g=A.
On the line c=++a;
the value of a
is pre-incremented before the equals assignment to c
on the left side. Therefore, a
was increased to 2
before assigning the value to c
, so this line is equivalent to two lines: a=a+1; c=a;
. However, on the line d=b++;
the value of b
is post-incremented and is equivalent to two lines:
d=b; b=b+1;
. The value of d
is assigned the value of b
, which is 2
, before the value of b
is incremented to 3
.
On the line e=(int)f;
a C-style cast is being used to convert a floating-point number into an integer value. Effectively, when programmers use a cast, they are notifying the compiler that they are aware that there will be a loss of precision in the conversion of a floating-point number to an int
(and that the compiler will introduce conversion code). The fractional part will be truncated, so 9.9999
is converted to e=9
, as the.9999
is removed by the truncation. One other point to note is that the printf("%.2f",f)
displays the floating-point variable to two decimal places, in contrast, rounding the value.
On the line g='A'
, g
is assigned the ASCII equivalent value of capital A, which is 65
. The printf("%d %c",g, g);
will display either the int
value of g
if %d
is used or the ASCII character value of g
if %c
is used.
A const
keyword can be used to prevent a variable from being changed. There is also a volatile
keyword that is useful for notifying the compiler that a particular variable might be changed outside its control and that the compiler should not apply any type of optimization to that value. This notification is useful on the Beagle board if the variable in question is shared with another process or physical input/output.
It is possible to define your own type in C/C++ using the typedef
keyword. For example, if you did not want to include the header file stdbool.h
in the sizeofvariables.c
previous example, it would be possible to define it in this way instead:
typedef char bool;
#define true 1
#define false 0
Probably the most common and most misunderstood mistake in C/C++ programming is present in the following:
if (x=y){
// perform a body statement Z
}
When will the body statement Z
be performed? The answer is whenever y
is not equal to 0
(the current value of x
is irrelevant!). The mistake is placing a single =
(assignment) instead of ==
(comparison) in the if
statement. The assignment operator returns the value on the RHS, which will be automatically converted to true
if y
is not equal to 0
. If y
is equal to zero, then a false
value will be returned. Java does not allow this error, as there is no implicit conversion between 0
and false
and 1
and true
.
A pointer is a special type of variable that stores the address of another variable in memory—we say that the pointer is “pointing at” that variable. Listing 5-12 is a code example that demonstrates how you can create a pointer p
and make it point at the variable y
.
When this code is compiled and executed, it will give the following output:
/chp05/overview$ ./pointers
The variable has value 1000 and the address 0xbede26bc.
The pointer stores 0xbede26bc and points at value 1000.
The pointer has address 0xbede26b8 and size 4.
So, what is happening in this example? Figure 5-3 illustrates the memory locations and the steps involved. In step 1, the variable y
is created and assigned the initial value of 1000
. A pointer p
is then created with the dereference type of int
. In essence, this means that the pointer p
is being established to point at int
values. In step 2, the statement p = &y;
means “let p
equal the address of y
,” which sets the value of p
to be the 32-bit address 0xbede26bc
. We now say that p
is pointing at y
. These two steps could have been combined using the call int *p = &y;
(i.e., create a pointer p
of dereference type int
and assign it to the address of y
).
Why does a pointer need a dereference type? For one example, if a pointer needs to move to the next element in an array, then it needs to know whether it should move by four bytes, eight bytes, etc. Also, in C++ you need to be able to know how to deal with the data at the pointer based on its type. Listing 5-13 is another example of working with pointers that explains how a simple error of intention can cause serious problems.
This will give the output as follows:
debian@ebb:~/exploringbb/chp05/overview$ ./pointers2
The pointer p has the value 1000 and address: 0xbeef357c
The pointer p has the value 1005 and address: 0xbeef3580
The variable z has the value 1005
In this example, the pointer p
is of dereference type int
, and it is set to point at the address of y
. At this point in the code the output is as expected, as p
has the “value of” 1000
and the “address of” 0xbeef357c
. On the next line the intention may have been to increase (post-increment) the value of y
by 1
to 1001
and assign z
a value of 1005
(i.e., before the post-increment takes place). However, perhaps contrary to your intention, p
now has the “value of” 1005
and the “address of” 0xbeef3580
.
Why has this occurred? Part of the difficulty of using pointers in C/C++ is understanding the order of operations in C/C++, called the precedence of the operations. For example, if you write the statement
int x = 1 + 2 * 3;
what will the value of x
be? In this case, it will be 7, because in C/C++ the multiplication operator has a higher level of precedence than the addition operator. Similarly, the problem in Listing 5-13 is your possible intention of using *p++
to increment the “value of” p
by 1
.
In C/C++ the post-increment operator (p++
) has precedence over the dereference operator (*p
). This means that *p++
actually post-increments the “address of” the pointer p
by one int
(i.e., 4 bytes), but before that, it is dereferenced (as 1000
in this example) so that it is added to 5
and assigned to z
(as visible on the third output line). Most worrying is the second output line, as it is clear that p
is now “pointing at” z
, which just happens to be at the next address—it could actually refer to an address outside the program's memory allocation. Such errors of intention are difficult to debug without using the debugging tools that are described in Chapter 7. To fix the code to suit your intention, simply use (*p)++
, which makes it clear that it is the “value of” p
that should be post-incremented by 1, resulting in p
having the “value of” 1001
and z
having the value 1005
. Should this change be applied, the pointer p
would not increment its address.
There are approximately 58 operators in C++, with 18 different major precedence levels. Even if you know the precedence table, you should still make it clear for other users what you intend in a statement by using round brackets (()
), which have the highest precedence level after the scope resolution (::
), increment (++
), and decrement (--
) operators. Therefore, you should always write the following:
int x = 1 + (2 * 3);
Finally, on the topic of C pointers, there is also a void* pointer that can be declared as void *p;
, which effectively states that the pointer p
does not have a dereference type and it will need to be assigned at a later stage (see /chp05/overview/void.c
) using the following syntax:
int a = 5;
void *p = &a;
printf("p points at address %p and value %d ", p, *((int *)p));
When executed, this code will give an output like the following:
The pointer p points at address 0xbea546c8 and value 5
Therefore, it is possible to cast a pointer from one dereference type to another, and the void pointer can potentially be used to store a pointer of any dereference type. In Chapter 6 void pointers are used to develop an enhanced GPIO interface.
The C language has no built-in string type but rather uses an array of the character type, terminated by the null character (