I went back to the 90s when I tested how to create a local Desktop app for PCs. Compilers targeting native binaries, O.S. default user interface and a single file database are uncommon today. However, there has always been a project with those requirements even though it is rare.
Unfortunately, most of modern solutions for local desktop apps are basically a browser packed with a javascript app that looks like a native one. Technologies like Python and Javascript are very productive, friendly for newcomers, easy to debug and run everywere. The price paid for these advantages is the big waste of resources with higher memory and CPU usage. Your software would require an interpreter, virtual machine or browser. Program instructions are typically scripts or virtual machine bytecode or some virtual machine instructions instead of the machine code itself.
We are living in a world with many M1 and M2 processors, low-cost computers with 512Gb SSD and most people have a multi-core computer in their pockets. Performance and light bundles might not be a strict requirement for your project. Anyway, if you are starting a new project with these requirements, the stack I have used could be a good fit.
Some default CRUD
The problem to be solved is a standard CRUD (Create Retrieve Update Delete) of a movie list. The columns of each movie are just its name, year of release, genre and score.
This is just a silly problem created in order to test the stack perform.
This app is coded for Linux. It can be easily executed on Windows with WSL2 or on MAC using VM or container.
Raw lightweight stack
The old C programming language is used aside the good old GCC compiler. Here lies the challenge. No garbage collector, no object orientation, no types, no namespaces and manual memory management. For these reasons it got fast. It is hard, unproductive and a lot of coders will hate it. This is the cost.
Beside the standard lib, the SQlite3 and Gtk3 are used to create a SQL database repository and user interface.
Repository
The SQLite3 database is a efficient, free and open source as well known. Its library was linked in the project. The Sqlite lib provides a connection that requires a filepath to the local storage. If the file does not exists, it will be creating. It also provides procedures to create SQL queries and prepared statements as well.
Some wrapper functions and one struct were wrote to manage the database storage. The struct is the following one:
typedef struct {
sqlite3 * db;
unsigned int connected;
char ** result_buffer;
int result_buffer_size;
} repository_sqlite3_s;
repository_sqlite3_s repository_sqlite3;
Wrapper procedures:
int repository_sqlite3_query(char * raw_query);
int repository_sqlite3_prepared_statement_query(const char *query_template, const char **bind_values, int bind_count);
The property repository_sqlite3.db
holds the pointer to the database connection. connected
is just the connection flag.
result_buffer
contains a pointer to the memory address of the query result first collumn. Adding a value to this variable (like result_buffer + 1
) you can get the address of the second column. In that way, adding the total column count, we could access the second row. Adding 15 * column_count
it would be accessing the 15th row.
result_buffer_size
is the column count.
Every time repository_sqlite3_prepared_statement_query
is called and returns 0
, the result is stored in the memory buffer as explained before.
Migrations
Migration is a good technique to control sql database versions. Each new table or column created, update or deleted is added to a list of changes. Every instance of the app needs to run all changes on the list. A new version of the app may have new items in its list.
I have never imagined I would code a database migration system in plain C but there it is:
int repository_sqlite3_migration_run()
{
int rc;
char *errMsg = 0;
printf("SQLite database migration...\n");
repository_sqlite3_connect();
import_migrations();
rc = sqlite3_exec(repository_sqlite3.db, SQL_MIGRATION_JOURNAL_CREATE_TABLE, 0, 0, &errMsg);
if (rc != SQLITE_OK)
{
printf(stderr, "SQL error: %s\n", errMsg);
sqlite3_free(errMsg);
return 1;
}
rc = repository_sqlite3_query(SQL_MIGRATION_JOURNAL_FETCH);
if (rc != SQLITE_OK)
{
printf(stderr, "SQL error: %s\n", errMsg);
sqlite3_free(errMsg);
return 1;
}
int * migrated_ids = malloc(sizeof(int) * repository_sqlite3.result_buffer_size);
for (int j=0; j<repository_sqlite3.result_buffer_size; j++)
{
char * migrated_id_str = repository_sqlite3.result_buffer[j];
migrated_ids[j] = atoi(migrated_id_str);
}
int * migrations_done = malloc(sizeof(int) * REPOSITORY_DATABASE_MIGRATION_BUFFER_SIZE);
for (int i=0; i<repository_database_migration_buffer_count; i++)
{
int already_migrated = 0; // TODO
for (int j=0; j<repository_sqlite3.result_buffer_size; j++)
if (migrated_ids[j] == i)
{
already_migrated = 1;
break;
}
if (already_migrated) continue;
repository_sqlite3_query(repository_database_migration_buffer[i]);
char * insert_migration_journal_sql_2 = app_strcat("INSERT INTO migration_journal (migration) VALUES('", int32_to_str(i));
char * insert_migration_journal_sql = app_strcat(insert_migration_journal_sql_2, "');");
repository_sqlite3_query(insert_migration_journal_sql);
free(insert_migration_journal_sql_2);
free(insert_migration_journal_sql);
}
repository_sqlite3_connect_clear_all_migrations();
free(migrations_done);
return 0;
}
GTK View
Gnome Tool Kit is a free, open source and complete UI lib written in C to create native user interfaces. It is used to create softwares like Gimp and Transmission (Bittorrent client). This is the other lib linked to the app. I have tested the tool kit before and its result can be found on this article.
A GTK visual editor called Glade was managed to design the window. It exports the result in a XML text file very similar to HTML.
<?xml version="1.0" encoding="UTF-8"?>
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.24"/>
<object class="GtkAdjustment" id="adjustment1">
<property name="lower">1</property>
<property name="upper">10</property>
<property name="step-increment">10</property>
<property name="page-increment">10</property>
</object>
<object class="GtkWindow" id="movies_window">
<property name="can-focus">False</property>
<property name="title" translatable="yes">Movies</property>
<property name="resizable">False</property>
<property name="destroy-with-parent">True</property>
<child>
<object class="GtkBox" id="main_window_container">
<property name="width-request">600</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="form_container">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">10</property>
<property name="margin-end">10</property>
<property name="margin-top">10</property>
<property name="margin-bottom">10</property>
<property name="orientation">vertical</property>
<property name="baseline-position">top</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
.....
</object>
</interface>
A GTK struct called builder is able to load the file mentioned by the gtk_builder_add_from_file
.
movies_view.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
GtkBuilder *builder = gtk_builder_new();
if (!gtk_builder_add_from_file(builder, "./glade/movies_view.glade", NULL))
{
g_printerr("Error loading Glade file\n");
return;
}
movies_view.window = GTK_WIDGET(gtk_builder_get_object(builder, "movies_window"));
if (!movies_view.window)
{
g_printerr("Error getting window object\n");
g_object_unref(builder);
return;
}
The results
The example full code is published in this Github repository: https://github.com/misabitencourt/gtk3-sqlite3-basic
By using a raw, simpler and layer lowered technologies we are able to build faster, headless and lightweight apps but its cost can be expensive. Tools like Angular and React are easier and way more productive than this to craft user interfaces. About the business and repository layers that could be used as server backend too, the difficulty persists even there. If it were decided to code with a language with garbage collector or some interpreted one, it would be coded around 4 times faster (based on my own experience).
The cons aren’t limited to development time. This app compiled binary runs only on Linux environments. Despite all of other platforms can emulate it easily, other technologies with virtual machines or interpreters bring us more resources to port our apps for other platforms reducing issues.
If you are looking for a high performance approach for your software, it would be a good fit! In other situations, You might think twice before choosing this kind of stack.