Integrating Rust in a C code

Integrating Rust in a C code

As a new software developer, I'm constantly investigating how to strengthen my skills and keep up with industry trends. Recently, I have read that Linux, has added support for developing with Rust aiming to attract younger developers to the project. So, I decided to challenge myself and see if I could integrate Rust into a C code base. In this article, I'll share my journey of exploring the intersection between these two languages and the lessons I learned along this short of the way.

The beginning

Motivated by the recent Rust support by the Linux kernel, I have decided to understand how the integration of C to Rust would work. The main idea was to get a functional project written in C, for which I have chosen LXTasks, to add the smallest feature I could, that, in this case, was the bar's monitor for GPU and VRAM.

LXTasks was the simplest code base I found that met my C skills. I, like many other developers, had contact with C only in college or when learning programming. Due to that, the knowledge I have about C is more generic and less practical, which made me concerned about how complex the code that I would decide should be. Thinking this way, LXTasks fit well with my requirements.

I've chosen the code because I’ve considered it to be the best fit for my object, meeting all the requirements I was looking for simple, functional, and written in C. So, the first step was trying to add the smallest code I can: a simple function that returns a number. To achieve that, as I read, I would need a head file with the function signature to be included and an object file with the implementation to be linked.

A header file contains C declarations and macro definitions that can be shared between multiple source files. There are two types of header files: system header files and “user” header files. System header files provide the interfaces to parts of the operating system, and they are used to provide the definitions and declarations needed to invoke system calls and libraries. User header files contain declarations for interfaces between the source files of your program.

#ifndef __EXTERNAL_H__
#define __EXTERNAL_H__

int get_int();

#endif

In my case, It is a “user” header file containing the necessary declarations that the object file compiled from Rust should meet for the linking process to be successful.

Object file

According to Object File article, "An object file is a computer file containing object code, that is, machine code output of an assembler or compiler. The object code is usually relocatable, and not usually directly executable. There are various formats for object files, and the same machine code can be packaged in different object file formats. An object file may also work like a shared library. [...] A linker is then used to combine the object code into one executable program or library, pulling in precompiled system libraries as needed.”

#[no_mangle]
pub extern "C" fn get_int() -> std::ffi::c_int {
    return 1;
}

With the code ready for the first test, built the project with the object file created from Rust. To that, I’ve written a simple and small Shell script to compile, and link everything to produce the final executable binary. This script has used the rustc, Rust compiler, to generate the object file called debug.o inside src/, and the GCC, GNU Compiler Collection, to all objects files from C.

All objects files compiled, I’ve to link everything together and build the final lxtask Elf file itself.

#!/bin/env sh

# rm src/debug.o
rustc src/debug.rs --crate-type staticlib --emit obj -o src/debug.o
if [ $? -ne 0 ]; then
    echo "Failed to compile external.rs"
    exit 1
fi

for i in src/*.c; do
rm ${i%.c}.o
gcc -c $i -o ${i%.c}.o -DVERSION=1 -pthread -I/usr/include/gtk-2.0 -I/usr/lib/gtk-2.0/include -I/usr/include/pango-1.0 -I/usr/include/glib-2.0 -I/usr/lib/glib-2.0/include -I/usr/include/sysprof-4 -I/usr/include/harfbuzz -I/usr/include/freetype2 -I/usr/include/libpng16 -I/usr/include/libmount -I/usr/include/blkid -I/usr/include/fribidi -I/usr/include/cairo -I/usr/include/pixman-1 -I/usr/include/gdk-pixbuf-2.0 -I/usr/include/atk-1.0 -I/usr/include/xmms2 -lnotify -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0
    if [ $? -ne 0 ]; then
        echo "Failed to compile $i"
        exit 1
    fi
 done

gcc src/*.o -o lxtask -lm -pthread -lnotify -lgdk_pixbuf-2.0 -lgio-2.0 -lgobject-2.0 -lglib-2.0 -lgtk-x11-2.0 -lgdk-x11-2.0 -lpangocairo-1.0 -latk-1.0 -lcairo -lpangoft2-1.0 -lpango-1.0 -lharfbuzz -lfontconfig -lfreetype -Wl,--export-dynamic -lgmodule-2.0 -lxmmsclient -lxmmsclient-glib -lstd
if [ $? -ne 0 ]; then
    echo "Failed to link"
    exit 1
fi

echo "Build successful"

Elf file

An Elf file, Executable and Linkable Format, according to IBM, “[…] is the standard binary format on operating systems such as Linux. Some of the capabilities of ELF are dynamic linking, dynamic loading, imposing run-time control on a program, and an improved method for creating shared libraries. The ELF representation of control data in an object file is platform-independent, which is an additional improvement over previous binary formats.” On its page on Wikipedia, it increases that “[…] is a common standard file format for executable files, object code, shared libraries, and core dumps”.

To check if the process has been successful, I’ve tried to execute the binary directly using:

./lxtask

Success! But now it's time to implement something useful! Of course, it wasn’t a straight way. The real process was heavy-based on learning and reminding many things about C like compiling and linking process, tooling etc. Beyond that, it was necessary to copy the Rust standard lib to the appropriate directly to run the binary file.

Starting to work on LXTask

Normally, before beginning to work with a new code base, I try to use it to understand its features and characteristics. I did the same here, looking for strings that could help me find where I need to change for improving a phrase, adding a new column or button, or inserting the bars I was willing.

Adding the bars was easy in some way; I just needed to find the code for the CPU and Memory bars, understand how it works superficially and replicate the same structure to the new ones.

The diffs

diff --git a/src/interface.c b/src/interface.c
index 5209193..d00018b 100644
--- a/src/interface.c
+++ b/src/interface.c
@@ -33,8 +33,12 @@ GtkWidget *mainmenu;
 GtkWidget *taskpopup;
 GtkWidget *cpu_usage_progress_bar;
 GtkWidget *mem_usage_progress_bar;
+GtkWidget *gpu_usage_progress_bar;
+GtkWidget *gpu_mem_usage_progress_bar;
 GtkWidget *cpu_usage_progress_bar_box;
 GtkWidget *mem_usage_progress_bar_box;
+GtkWidget *gpu_usage_progress_bar_box;
+GtkWidget *gpu_mem_usage_progress_bar_box;

 GtkTreeViewColumn *column;

@@ -62,6 +66,7 @@ GtkWidget* create_main_window (void)
     GtkWidget *button3;

     GtkWidget *system_info_box;
+    GtkWidget *gpu_info_box;

     window = gtk_window_new (GTK_WINDOW_TOPLEVEL);
     gtk_window_set_title (GTK_WINDOW (window), _("Task Manager"));
@@ -128,6 +133,10 @@ GtkWidget* create_main_window (void)
     gtk_widget_show (system_info_box);
     gtk_box_pack_start (GTK_BOX (vbox1), system_info_box, FALSE, TRUE, 0);

+    gpu_info_box = gtk_hbox_new (TRUE, 10);
+    gtk_widget_show (gpu_info_box);
+    gtk_box_pack_start (GTK_BOX (vbox1), gpu_info_box, FALSE, TRUE, 0);
+
     cpu_usage_progress_bar_box = gtk_event_box_new ();
     cpu_usage_progress_bar = gtk_progress_bar_new ();
 #if GTK_CHECK_VERSION(3,0,0)
@@ -150,6 +159,28 @@ GtkWidget* create_main_window (void)
     gtk_container_add (GTK_CONTAINER (mem_usage_progress_bar_box), mem_usage_progress_bar);
     gtk_box_pack_start (GTK_BOX (system_info_box), mem_usage_progress_bar_box, TRUE, TRUE, 0);

+    gpu_usage_progress_bar_box = gtk_event_box_new ();
+    gpu_usage_progress_bar = gtk_progress_bar_new ();
+#if GTK_CHECK_VERSION(3,0,0)
+    gtk_progress_bar_set_show_text (GTK_PROGRESS_BAR (gpu_usage_progress_bar), TRUE);
+#endif
+    gtk_progress_bar_set_text (GTK_PROGRESS_BAR (gpu_usage_progress_bar), _("gpu usage"));
+    gtk_widget_show (gpu_usage_progress_bar);
+    gtk_widget_show (gpu_usage_progress_bar_box);
+    gtk_container_add (GTK_CONTAINER (gpu_usage_progress_bar_box), gpu_usage_progress_bar);
+    gtk_box_pack_start (GTK_BOX (gpu_info_box), gpu_usage_progress_bar_box, TRUE, TRUE, 0);
+
+    gpu_mem_usage_progress_bar_box = gtk_event_box_new ();
+    gpu_mem_usage_progress_bar = gtk_progress_bar_new ();
+#if GTK_CHECK_VERSION(3,0,0)
+    gtk_progress_bar_set_show_text (GTK_PROGRESS_BAR (gpu_mem_usage_progress_bar), TRUE);
+#endif
+    gtk_progress_bar_set_text (GTK_PROGRESS_BAR (gpu_mem_usage_progress_bar), _("gpu memory usage"));
+    gtk_widget_show (gpu_mem_usage_progress_bar);
+    gtk_widget_show (gpu_mem_usage_progress_bar_box);
+    gtk_container_add (GTK_CONTAINER (gpu_mem_usage_progress_bar_box), gpu_mem_usage_progress_bar);
+    gtk_box_pack_start (GTK_BOX (gpu_info_box), gpu_mem_usage_progress_bar_box, TRUE, TRUE, 0);
+
     scrolledwindow1 = gtk_scrolled_window_new (NULL, NULL);
     gtk_widget_show (scrolledwindow1);
     gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolledwindow1), GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
diff --git a/src/interface.h b/src/interface.h
index df9c602..2cb1130 100644
--- a/src/interface.h
+++ b/src/interface.h
@@ -45,8 +45,12 @@ extern GtkWidget *mainmenu;
 extern GtkWidget *taskpopup;
 extern GtkWidget *cpu_usage_progress_bar;
 extern GtkWidget *mem_usage_progress_bar;
+extern GtkWidget *gpu_usage_progress_bar;
+extern GtkWidget *gpu_mem_usage_progress_bar;
 extern GtkWidget *cpu_usage_progress_bar_box;
 extern GtkWidget *mem_usage_progress_bar_box;
+extern GtkWidget *gpu_usage_progress_bar_box;
+extern GtkWidget *gpu_mem_usage_progress_bar_box;

 enum {
     COLUMN_NAME = 0,

Now, I had two extra bars, but without any functionality. From here, Rust take its place back on this project. As said at the beginning of this post, to enable the C code calls the Rust one, I had to create a header file with the functions signatures to be linked to the final elf file.

diff --git a/src/external.h b/src/external.h
new file mode 100644
index 0000000..d74c2ae
--- /dev/null
+++ b/src/external.h
@@ -0,0 +1,6 @@
+#ifndef __EXTERNAL_H__
+#define __EXTERNAL_H__
+
+struct GPUInfo get_gpu_info();
+
+#endif
diff --git a/src/functions.h b/src/functions.h
index 5829b43..005cf77 100644
--- a/src/functions.h
+++ b/src/functions.h
@@ -42,8 +42,15 @@
 #define PROC_DIR_2 "/emul/linux/proc"
 #define PROC_DIR_3 "/proc"

+struct GPUInfo {
+    gdouble gpu_usage;
+    gint gpu_mem_total;
+    gint gpu_mem_used;
+};
+
 gboolean refresh_task_list(void);
 gdouble get_cpu_usage(system_status *sys_stat);
+struct GPUInfo get_gpu_info();

 /* Configurationfile support */
 void load_config(void);

With the function header and structure defined, I began to code the functionality using Rust. The idea was not to be perfect, not even multi-branding, but to make it work to test the integration. That's why the code looks so dumb and tied to the nvidia-smi tool, which I have used as their output for the data.

diff --git a/src/external.rs b/src/external.rs
new file mode 100644
index 0000000..7a10948
--- /dev/null
+++ b/src/external.rs
@@ -0,0 +1,69 @@
+#[repr(C)]
+pub struct GPUInfo {
+    pub gpu_usage: std::ffi::c_double,
+    pub gpu_mem_total: std::ffi::c_int,
+    pub gpu_mem_used: std::ffi::c_int,
+}
+
+impl GPUInfo {
+    pub fn from_vec(vec: Vec<&str>) -> Self {
+        Self {
+            gpu_usage: vec[0].parse::<f64>().unwrap() / 100.0,
+            gpu_mem_total: vec[1].parse::<i32>().unwrap(),
+            gpu_mem_used: vec[2].parse::<i32>().unwrap(),
+        }
+    }
+}
+
+impl Default for GPUInfo {
+    fn default() -> Self {
+        Self {
+            gpu_usage: 0.0,
+            gpu_mem_total: 0,
+            gpu_mem_used: 0,
+        }
+    }
+}
+
+fn nvidia_gpu_info() -> Result<GPUInfo, std::io::Error> {
+    let data = std::process::Command::new("nvidia-smi")
+        .arg("--query-gpu=utilization.gpu,memory.total,memory.used")
+        .arg("--format=csv,noheader,nounits")
+        .output();
+
+    match data {
+        Ok(data) => {
+            let string = data
+                .stdout
+                .iter()
+                .map(|&x| x as char)
+                .collect::<String>()
+                .trim()
+                .to_string()
+                .to_owned()
+                .clone();
+
+            let info = string.split(", ").collect::<Vec<&str>>();
+
+            return Ok(GPUInfo::from_vec(info));
+        }
+        Err(err) => {
+            return Err(err);
+        }
+    }
+}
+
+/// Get GPU info returns a GPUInfo struct what contains the GPU usage, total memory and used memory.
+/// If the GPU is not found, it returns a default GPUInfo struct.
+///
+/// For now, only Nvidia GPUs are supported. If you want to add support for other GPUs, please open a PR.
+#[no_mangle]
+pub extern "C" fn get_gpu_info() -> GPUInfo {
+    let gpu_info = nvidia_gpu_info();
+
+    if let Ok(gpu_info) = gpu_info {
+        return gpu_info;
+    } else {
+        return GPUInfo::default();
+    }
+}

The result

After introducing some changes to the build.sh script, coping the Rust dynamic library to the right path to build successfully, and finally, I could see it working. The code changes were not so impressive, and it was the goal. My challenge was trying to integrate them two, but in some kind of usual feature, even if it was simple. I got it!

Did you find this article valuable?

Support Henry Barreto by becoming a sponsor. Any amount is appreciated!