Modern Software Development Tools and oneAPI Part 2
Modern Software Development Tools and oneAPI Part 2
This is part 2 of using a modern open source toolchain to build oneAPI based applications. If this is the first time you're seeing this post, you can read part one here
In part one, we talked about how easy it is to put a container together that will allow you to have everything you need to compile and build an oneAPI application. However, the sample code was compiled simply with:
clang++ -fsycl simple-oneapi.cpp -o simple-sycl
Not particularly interesting, is it?
This post will focus on building this same code using Meson. Meson focuses on simplicity, is a build system generator, but has a concept of back-ends where it can generate whatever the back-end defines. Together with the backend creates a featureful build system. Currently, the default back-end is the fabulous ninja on the Linux platform. Ninja is a command runner that is extremely fast compared to something like Make. Meson also supports xcode and vscode as back-ends allowing to easily use Meson on MacOS and Windows.
Meson just recently hit 1.0 after ten years of development. A wonderful milestone. You can read about it in this blog post.
Installing Meson and Ninja
To get Meson working, we first need to install its prerequisites. This is fairly easy to do, assuming that you are not in the oneAPI container.
$ distrobox enter oneapi
$ pip3 install --user meson
$ pip3 install --user ninja
You are, of course, welcome to install them site-wide so you don't clutter up your home directory and also, have a clear delineation between toolchain in the container and in your regular host. Make sure that you set your PATH to include /usr/local/bin.
Now we have Meson and the ninja build system ready to go!
Building a simple sycl app with Meson
Let's start with a fresh directory. The sample sycl code is simple and easy enough to turn into a Meson-based project. Please keep in mind that this is not meant to be a complete tutorial on Meson. If you have questions, please respond to the blog post and I will do my best to answer.
$ mkdir -p $HOME/src/simple-oneapi
$ cd $HOME/src/simple-oneapi
The first thing to do is to write a meson.build file in the top level directory. The meson.build file will contain the following:
project('simple-oneapi', ['cpp', 'c'],
version: '0.1.0',
meson_version: '>= 0.59.0',
default_options: [ 'warning_level=2', 'werror=false', 'cpp_std=gnu++2a', ],
)
subdir('src')
This file will define the project and its prerequisites. From here, you can see that we are defining a project that will use the C++ language version and requires a Meson version above 0.59.0. You can also define what the default compiler options are for the project. Finally, it defines that there are other sub directories with code.
For those who use make, it should be familiar to have each sub-directory have its own meson.build files to define how the code in that directory will be built. This will be no different.
$ mkdir src
$ cd src
Since we defined a src sub-directory, we're going to go ahead and create one and then add our simple oneapi source code and a meson file.
Create a file called simple-oneapi.cpp with this source code:
#include <sycl/sycl.hpp>
int main() {
// Creating buffer of 4 ints to be used inside the kernel code
sycl::buffer<sycl::cl_int, 1> Buffer(4);
// Creating SYCL queue
sycl::queue Queue;
// Size of index space for kernel
sycl::range<1> NumOfWorkItems{Buffer.size()};
// Submitting command group(work) to queue
Queue.submit([&](sycl::handler &cgh) {
// Getting write only access to the buffer on a device
auto Accessor = Buffer.get_access<sycl::access::mode::write>(cgh);
// Executing kernel
cgh.parallel_for<class FillBuffer>(
NumOfWorkItems, [=](sycl::id<1> WIid) {
// Fill buffer with indexes
Accessor[WIid] = (sycl::cl_int)WIid.get(0);
});
});
// Getting read only access to the buffer on the host.
// Implicit barrier waiting for queue to complete the work.
const auto HostAccessor = Buffer.get_access<sycl::access::mode::read>();
// Check the results
bool MismatchFound = false;
for (size_t I = 0; I < Buffer.size(); ++I) {
if (HostAccessor[I] != I) {
std::cout << "The result is incorrect for element: " << I
<< " , expected: " << I << " , got: " << HostAccessor[I]
<< std::endl;
MismatchFound = true;
}
}
if (!MismatchFound) {
std::cout << "The results are correct!" << std::endl;
}
return MismatchFound;
}
Your src directory should look like:
$ ls
simple-oneapi.cpp
Now, we are going to create a meson.build file to handle compiling simple-oneapi.cpp.
Recall that we compiled this source code using clang++ -fsycl - so we're going to have to duplicate that behavior.
Create a meson.build file with this content:
simple_oneapi_sources = files('simple-oneapi.cpp')
simple_oneapi_deps = [
]
executable('simple-oneapi', simple_oneapi_sources,
link_args:'-fsycl',
cpp_args:'-fsycl',
dependencies: simple_oneapi_deps,
install: true, install_dir: '/var/home/sri/Projects/simple-oneapi/bin'
)
This demonstrates how easy to understand the syntax of Meson is, and it contributes quite a bit to maintainability, especially if your build system gets more complex.
Define our source code
simple_oneapi_sources = files('main.cpp')
This tells Meson what source files you have. It'll be a comma delineated list of sources.
You can define more source code files like so:
simple_oneapi_sources = files('simple-oneapi.cpp', 'aux1.cpp')
Define our dependencies
simple_oneapi_deps = [
]
This will be a list of dependencies for this project. In this case, we don't have any dependencies as this is a fairly simple example.
The dependencies are generally discovered through pkg-config
. If you wanted to add a dependency, it would look something like this:
simple_oneapi_deps = [
dependency('zlib'),
dependency('cups', method: 'pkg-config'),
]
See, the Meson documentation on dependencies for more information on dependencies.
Getting back to our example:
Defining the final compile
We now want to create a binary executable that will ultimately put our sources and the required dependencies and compile them all together.
executable('simple-oneapi', simple_oneapi_sources,
link_args:'-fsycl',
cpp_args:'-fsycl',
dependencies: simple_oneapi_deps,
install: true, install_dir: '/var/home/sri/Projects/simple-oneapi/bin'
)
We define our executable to be the sources we have defined, with the linker and pre-processor flags required. Finally, a location of where to put the resulting binary when we want to install it.
That's pretty much it. What made this tricky is that SYCL is a define-your-own-environment and so, we were not able to take advantage of a lot of the built-ins that Meson has. Instead, we had to set everything up manually. For instance, the link_args was required in order to compile the object files in the final compile.
Setting up our build system
We now have all the elements to put together our build system and getting our compile going. Let's see how we can do that.
Let's go back to the top directory of our project.
$ cd ~/src/simple-oneapi
To create this build system, we need to make sure that we set the right compiler. In this case, we are using clang++. Most of you should already be familiar with using environment variables to set up the environments.
$ CC=clang CXX=clang++ meson setup builddir
Meson does not support in-tree source code builds, so you must always define a build directory.
The result should look like this:
The Meson build system
Version: 1.0.0
Source dir: /var/home/sri/Projects/simple-oneapi
Build dir: /var/home/sri/Projects/simple-oneapi/builddir
Build type: native build
Project name: simple-oneapi
Project version: 0.1.0
C compiler for the host machine: clang (clang 16.0.0 "clang version 16.0.0 (https://github.com/intel/llvm 08be083e07b1fd6437267e26adb92f1b647d57dd)")
C linker for the host machine: clang ld.bfd 2.34
C++ compiler for the host machine: clang++ (clang 16.0.0 "clang version 16.0.0 (https://github.com/intel/llvm 08be083e07b1fd6437267e26adb92f1b647d57dd)")
C++ linker for the host machine: clang++ ld.bfd 2.34
Host machine cpu family: x86_64
Host machine cpu: x86_64
Build targets in project: 1
Found ninja-1.11.1.git.kitware.jobserver-1 at /var/home/sri/.local/bin/ninja
We are now ready to build this simple oneapi project.
To build our project, we simply do:
$ cd builddir
$ ninja
The resultant binary will be built in the src/ directory. Alternatively, if you want to be consistent especially if you're using the same codebase to build on windows and let Meson figure out which backend to use.
$ cd builddir
$ meson compile
You can find the results in the src/ directory.
$ cd src
$ ./simple-oneapi
The results are correct!
So, now we have successfully built a simple oneapi binary using Meson!!
There are several possibilities to using Meson as a build system. Meson integrates well with CMake and other build systems, so you would not have to rebuild your system from scratch.
The greatest advantage of Meson is speed and simplicity on the Linux platform.
Interested in learning more about Meson and being part of the community? Find out more at https://mesonbuild.com/. There is a Meson community on Matrix - https://matrix.to/#/#mesonbuild:matrix.org. I also highly encourage you to read Jussi Pakkaneβ blog at https://nibblestew.blogspot.com/.
In the next and final blog post, we'll talk about how we can use GNOME Builder in conjunction with our container and Meson to finally put a user-friendly developer environment to write oneAPI applications on the Linux platform.