Wednesday, April 15, 2020

Cross Compiling C++ Programs for Raspberry Pi that Uses Static or Shared Libraries Such as OpenCV and wxWidgets

In this article, cross compiling C++ programs for Raspberry Pi on Ubuntu Linux system is discussed. We use "schroot" to avoid messing the main system and to get clean build environment.


Setting up

Install some required libraries, and get some tools as follows [1].

$ sudo apt install gnupg dirmngr curl sbuild \
ubuntu-dev-tools qemu-user-static binfmt-support


Check id for the current user and add it to sbuild group. Edit sudoers if you do not want password prompting. Logout and login again.

$ id 
$ sudo usermod -a -G sbuild $USER
$ sudo sh -c 'echo "$SUDO_USER ALL=(ALL) NOPASSWD: ALL" >> /etc/sudoers'


Get public key for raspbian archive and use gpg (OpenPGP encryption and signing tool) to create keyring used by debootstrap. Debootstrap is a tool which will install a Debian base system into a subdirectory of another, already installed system.

$ curl -sL http://archive.raspbian.org/raspbian.public.key | gpg --import -
$ gpg --export 9165938D90FDDD2E > $HOME/raspbian-archive-keyring.gpg


Create 'rpi.sources' file with a list as follows.

$ cat > $HOME/rpi.sources <<EOF
deb http://archive.raspbian.org/raspbian/ RELEASE main contrib non-free rpi
deb-src http://archive.raspbian.org/raspbian/ RELEASE main contrib non-free rpi
EOF


And setup mk-sbuild defining chroots directory, debootstrap keyring, and sources list.

$ cat > $HOME/.mk-sbuild.rc <<EOF
SOURCE_CHROOTS_DIR="$HOME/chroots"
DEBOOTSTRAP_KEYRING="$HOME/raspbian-archive-keyring.gpg"
TEMPLATE_SOURCES="$HOME/rpi.sources"
SKIP_UPDATES="1"
SKIP_PROPOSED="1"
SKIP_SECURITY="1"
EATMYDATA="1"
EOF


Create a full sbuild-with-schroot environment to do builds.

$ mk-sbuild --name stretch-rpi --arch=armhf \
--debootstrap-mirror=http://archive.raspbian.org/raspbian/ stretch


After the building is done, and you will see a message as shown below.


Figure 1. Building stretch-rpi-armhf


Use stream editor command 'sed' to replace 'union-type=aufs' with 'union-type=overlay' in 'sbuild-stretch-rpi-armhf' file in place.

$ sudo sed -i 's/union-type=aufs/union-type=overlay/g' \
/etc/schroot/chroot.d/sbuild-stretch-rpi-armhf


Using schroot

To bind a local directory into schroot, edit '/etc/schroot/sbuild/fstab' in nano,

$ sudo nano /etc/schroot/sbuild/fstab


and add a line similar to the one shown below.

/home/yan/ws    /home/yan/ws    none    rw,bind         0       0


List all schroot sessions you have as follows [2].

$ schroot -la


And begin a new schroot instance:

$ schroot -b -c stretch-rpi-armhf


That creats a new schroot environment and echos the session name like

stretch-rpi-armhf-7fedd5e2-c7aa-46fe-bcfc-b804606d5ba3


Then enter the created schroot environment above as a 'root' user.

$ schroot -r -u root -c stretch-rpi-armhf-7fedd5e2-c7aa-46fe-bcfc-b804606d5ba3


Now you are in a "sandbox" clean environment with the cross compilers installed as illustrated below.


Figure 2. Entering schroot environment.


You can exit from the environment using 'exit' command, and you can re-enter the session. It is also possible to create multiple schroot sessions. To clean up, you can end a session using '-e'. In that case, you will lose all files that you have added in that session.

$ schroot -e -c stretch-rpi-armhf-7fedd5e2-c7aa-46fe-bcfc-b804606d5ba3


To install software packages in the environment, you can use apt as shown below. You can append ':armhf' behind the package name also.

# apt update
# apt -y install cmake git pkg-config
# apt -y install libgtk-3-dev libavcodec-dev libavformat-dev libswscale-dev


As we are cross compiling, we want to use 'arm-linux-gnueabihf-gcc' or 'arm-linux-gnueabihf-g++'' instead of gcc or g++.

# which arm-linux-gnueabihf-gcc
# arm-linux-gnueabihf-gcc -v


Building a Simple Program

Let us start building a simple program, cc1.cpp, which is listed below.

#include <stdio.h>
int main (void)
{
  printf("Hello cross compiling!\n");
  return 0;
}


Save the file in our shared directory as "/home/yan/ws/cc1/cc1.cpp" so that we have access to it in both Ubuntu and schroot environment. Also make the "CMakeLists.txt" as usual.

cmake_minimum_required(VERSION 3.0)
project(cc1)
add_executable(cc1 cc1.cpp)


Then, create a tool chain file with a name like "pi.cmake" where we set compilers [3].

SET(CMAKE_SYSTEM_NAME Linux)
SET(CMAKE_SYSTEM_VERSION 1)
SET(CMAKE_C_COMPILER /usr/bin/arm-linux-gnueabihf-gcc)
SET(CMAKE_CXX_COMPILER /usr/bin/arm-linux-gnueabihf-g++)


Thereafter, we can cross compile the program with the option for the tool chain file.

# cd /home/yan/ws/cc1
# mkdir build
# cd build
# cmake -D CMAKE_TOOLCHAIN_FILE=../pi.cmake ..
# make


When you run the program, you can see the output of the program because we already have installed QEMU earlier.

# ls
# ./cc1
# objdump -l cc1
# objdump -i cc1
# ldd cc1
# cd ..


OpenCV

If we use shared libraries, the version on build machine and the host must be the same. Therefore, we will get a specific version of opencv, build from source, and install as follows. The cmake command might get error when you enter it first time and it should be OK if you enter the command again. Add "-D SOFTFP=ON" option if you want to use soft floating point. And if you want to build OpenCV as a shared library, change it to '-D BUILD_SHARED_LIBS=ON'.

# cd /home/yan/ws
# wget https://github.com/opencv/opencv/archive/4.2.0.tar.gz
# mv 4.2.0.tar.gz opencv-4.2.0.tar.gz
# tar zxvf opencv-4.2.0.tar.gz
# wget https://github.com/opencv/opencv_contrib/archive/4.2.0.tar.gz
# mv 4.2.0.tar.gz opencv_contrib-4.2.0.tar.gz
# tar zxvf opencv_contrib-4.2.0.tar.gz
# cd opencv-4.2.0/platforms/linux
# mkdir -p build
# cd build
# cmake -D CMAKE_BUILD_TYPE=Release -D CMAKE_INSTALL_PREFIX=/usr \
-D OPENCV_EXTRA_MODULES_PATH=../../../../opencv_contrib-4.2.0/modules \
-D CMAKE_TOOLCHAIN_FILE=../arm-gnueabi.toolchain.cmake \
-D BUILD_SHARED_LIBS=OFF ../../..
# make
# make install
# echo /usr/lib/ > /etc/ld.so.conf.d/opencv.conf
# ldconfig


As an example, "cc2.cpp" uses opencv imread to load an image file, thiri.jpg, from the source directory, and shows the image.

#include <stdio.h>
#include <iostream>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;

//-----------------------------------------------------------------------------
int main(int argc, char** argv)
{
 Mat a = (Mat_<uchar>(2,3)<<0,1,2,3,4,5);
 cout << a << endl;

 Mat f = Mat_<float>(a);
 f = f * 2.0;
 cout << f << endl;

 Mat c;
 normalize(f,f,0,255, NORM_MINMAX);
 c = Mat_<uchar>(f);
 cout << c << endl;
 
 return 0;
}
//-----------------------------------------------------------------------------


Its "CMakeLists.txt" is as follows.

cmake_minimum_required(VERSION 2.8)
project(cc2)
find_package( OpenCV REQUIRED )
add_executable( cc2 cc2.cpp )
target_link_libraries( cc2 ${OpenCV_LIBS} )


The same tool chain file in the previous example, "pi.cmake", is used again. And build:

# cd /home/yan/ws/cc2
# mkdir build
# cd build
# cmake -D CMAKE_TOOLCHAIN_FILE=../pi.cmake ..
# make


Copy the executable file from our build machine to the host Raspberry Pi, and run the program. The program should run properly even if OpenCV is not installed in the host. Because we built OpenCV as a static library using '-D BUILD_SHARED_LIBS=OFF' option.

wxWidgets

Similarly, we will download specific version of wxWigets and build it as static library. Change '--disable-shared' to '--enable-shared' if you want to build as shared library.

# cd /home/yan/ws
# wget https://github.com/wxWidgets/wxWidgets/releases/download/v3.0.4/wxWidgets-3.0.4.tar.bz2
# tar xjvf wxWidgets-3.0.4.tar.bz2
# cd wxWidgets-3.0.4
# mkdir gtk-build
# cd gtk-build
# ../configure --help
# ../configure --enable-unicode --disable-shared --with-gtk=3 --prefix=/usr \
--host=arm-unknown-linux-gnueabi --build=arm-unknown-linux-gnueabi
# make
# make install


A simple wxWidgets program, "cc3.cpp", and its "CMakeLists.txt" are shown in the following lists. The tool chain file "pi.cmake" from previous examples can be copied to the source folder also.

#include 
class Simple : public wxFrame
{
public:
    Simple(const wxString& title);

};
Simple::Simple(const wxString& title)
       : wxFrame(NULL, wxID_ANY, title, wxDefaultPosition, wxSize(250, 150))
{
  Centre();
}

class MyApp : public wxApp
{
  public:
    virtual bool OnInit();
};

IMPLEMENT_APP(MyApp)
bool MyApp::OnInit()
{
    Simple *simple = new Simple(wxT("Simple"));
    simple->Show(true);

    return true;
}




cmake_minimum_required(VERSION 2.8)
project(cc3)
find_package(wxWidgets COMPONENTS core net base REQUIRED)
include("${wxWidgets_USE_FILE}")
add_executable(cc3 cc3.cpp)
target_link_libraries(${PROJECT_NAME} ${wxWidgets_LIBRARIES})


Build it as follows.

# cd /home/yan/ws/cc3
# mkdir build
# cd build
# cmake -D CMAKE_TOOLCHAIN_FILE=../pi.cmake ..
# make


The resulting executable can be now copied to the host Raspberry Pi and run on it.

Acknowledgements

I would like to express my thanks to Michael Walton for his advice and guidance.

References

[1] R4SAS. Building sbuild environment for cross-building raspbian packages on amd64/i386 machines. 2018 MAR 05.
url: https://i2p.rocks/blog/building-sbuild-environment-for-cross-building-raspbian-packages-on-amd64i386-machines.html.

[2] Debian Wiki. CrossCompiling. 2020-03-12.
url: https://wiki.debian.org/CrossCompiling.

[3] Alex C.U. The Useful RaspberryPi Cross Compile Guide. 2017-08-01.
url: https://medium.com/@au42/the-useful-raspberrypi-cross-compile-guide-ea56054de187.

No comments:

Post a Comment

Comments are moderated and don't be surprised if your comment does not appear promptly.