Seite wählen

NETWAYS Blog

CGo. Halb C – halb Go.

In meinem letzten Blog-Post, The way to Go, habe ich bereits einige Vorteile der Programmiersprache Go aufgezeigt. Und als ob es nicht schon genug wäre, dass man beim Umstieg auf Go nicht gleich das Kind mit dem Bade ausschütten muss: Ein einzelnes Programm muss nicht mal komplett in Go geschrieben sein…

Multilingual unterwegs

Ich arbeite für einen deutschen IT-Dienstleister, aber beim recherchieren von Lösungen für Kundenprobleme muss ich die russischen Quellen nicht erst ins Deutsche übersetzen, sondern kann direkt mit ihnen arbeiten. Genau so kann bspw. ein Go-Programm direkt von C-Bibliotheken Gebrauch machen – und umgekehrt. Vorausgesetzt, die Schnittstellen beschränken sich auf den kleinsten gemeinsamen Nenner.

Was wäre das auch wirtschaftlich für ein Fass ohne Boden wenn sämtliche benötigte Bibliotheken erst in Go neu geschrieben werden müssten. Umso mehr wundert es mich, dass so manche Datenbank-Treiber doch komplett neu geschrieben wurden…

Good programmers know what to write.
Great ones know what to rewrite (and reuse).

Eric Steven Raymond, The Cathedral and the Bazaar

Multilingual in der Praxis

Ich bin zwar kein Fernsehkoch, aber habe trotzdem schon mal was vorbereitet – eine extrem abgespeckte Version von go-linux-sensors:

[code]
package go_linux_sensors

/*
#include <stdio.h>
#include <string.h>
#include <sensors/sensors.h>
#include <sensors/error.h>

typedef const char cchar;

#cgo LDFLAGS: -lsensors
*/
import "C"

import (
"reflect"
"unsafe"
)

type Error struct {
errnr C.int
}

func (e Error) Error() string {
return cStr2str(C.sensors_strerror(e.errnr))
}

func Init() error {
if err := C.sensors_init((*C.FILE)(nil)); err != C.int(0) {
return Error{err}
}

return nil
}

func Cleanup() {
C.sensors_cleanup()
}

func GetLibsensorsVersion() string {
return cStr2str(C.libsensors_version)
}

func cStr2str(cStr *C.cchar) string {
slen := int(C.strlen(cStr))

bridge := reflect.SliceHeader{
Data: uintptr(unsafe.Pointer(cStr)),
Len: slen,
Cap: slen,
}

return string(*(*[]byte)(unsafe.Pointer(&bridge)))
}
[/code]

Diese schlägt eine Brücke zu libsensors, um die Hardware-Sensoren auszulesen.

Direkt nach dem Package folgt auch schon der Import des ominösen Pakets „C“. Dieses enthält sämtliche C-Standard-Datentypen und alle Datentypen und Funktionen, die über Header-Dateien im Kommentar direkt darüber eingebunden wurden. Ja, richtig, in diesem „magischen“ Kommentar lässt sich nahezu beliebiger C-Code unterbringen, um auf jenen im Go-Code zuzugreifen. Des weiteren lassen sich die tatsächlichen Bibliotheken mit „#cgo LDFLAGS:“ automatisch linken. Bei höheren Mengen an Code/Includes bevorzuge ich persönlich eine extra Header-Datei statt einem langen Kommentar.

Etwas weiter unten ruft Init() auch schon sensors_init() auf – als wäre es eine gewöhnliche Go-Funktion im Paket C. Und auch die Datentypen lassen sich verwenden als seien es die eigenen – wovon Error fleißig Gebrauch macht.

Lediglich cStr2str() macht einen etwas unorthodoxen Eindruck… aber hey – es funktioniert. 😉

Jeder gesparte Euro zählt…

… und jede wiederverwendete Zeile Code. Wir bei NETWAYS wissen das zu schätzen und arbeiten dementsprechend effizient – an euren Projekten!

Alexander Klimov
Alexander Klimov
Senior Developer

Alexander hat 2017 seine Ausbildung zum Developer bei NETWAYS erfolgreich abgeschlossen. Als leidenschaftlicher Programmierer und begeisterter Anhänger der Idee freier Software, hat er sich dabei innerhalb kürzester Zeit in die Herzen seiner Kollegen im Development geschlichen. Wäre nicht ausgerechnet Gandhi sein Vorbild, würde er von dort aus daran arbeiten, seinen geheimen Plan, erst die Abteilung und dann die Weltherrschaft an sich zu reißen, zu realisieren - tut er aber nicht. Stattdessen beschreitet er mit der Arbeit an Icinga Web 2 bei uns friedliche Wege.

On giving up and trying an IDE

I dislike IDEs, at least I tell myself and others that. A 200 line long .vimrc gives a lot more street cred than clicking on a colored icon and selecting some profile that mostly fits ones workflow. So does typing out breakpoints in GDB compared to just clicking left of a line. Despite those very good reasons I went ahead and gave Goland and CLion, two JetBrains products for Go and C/C++ respectively, a chance. The following details my experiences with a kind of software I never seen much use for, the problems I ran into, and how it changed my workflow.

Installation and Configuration

A picture of my IDE wouldn’t do much good, they all look the same. So here’s a baby seal.
Source: Ville Miettinen from Helsinki, Finland


First step is always the installation. With JetBrains products being mostly proprietary, there are no repositories for easy installation and updating. But for the first time I had something to put in /opt. When going through the initial configuration wizard one plugin caught my eye: „IdeaVim“. Of course I decided to install and activate said plugin but quickly had to realize it does not work the same simply running vim in a window.
This „Vim emulation plug-in for IDEs based on the IntelliJ platform“ sadly does for one not offer the full Vim experience and the key bindings often clash with those of the IDE. Further I started getting bothered by having to manage the Vim modes when wanting to write code.
I sometimes miss the ability to easily edit and move text the way Vim allows, the time I spend using the mouse to mark and copy text using the mouse are seconds of my life wasted I’ll never get back!
Except for the underlying compiler and language specific things both IDEs work the same, identical layout, window options, and most plugins are shared, so I won’t talk about the tool chain too much. But it needs to be said that Goland just works, possibly because building a Go project seems simpler than a C++ one. Getting CLion to work with CMake is tiresome, the only way to edit directives is by giving them to the command like you would on the shell. This would not bother me as much if CMake didn’t already have a great GUI.

Coding and Debugging

Yet I wouldn’t be still using those programs if there weren’t upsides. The biggest being the overview over the whole project, easily finding function declarations and splitting windows as needed. These are things Vim can be made to do, but it does not work as seamless as it does in the IntelliJ world. It made me realize how little time is spent the actual typing of code, most of it is reading code, drawing things and staring at a prototype until your eyes bleed confusion (sometimes code is not well commented). The debuggers, again specifically the one of Goland, work great! Sometimes I have to talk to GDB directly since there are many functions but too few buttons, but the typical case of setting a breakpoint and stepping through to find some misplaced condition is simple and easy.

Alright, here it is.


There are a few features I have not found a use for yet e.g. code generators and I still manage my git repositories from the shell. The automatic formatting is cool, again especially in Go where there is one standard and one tool for it. Other than that I run into a few bugs now and then, one that proved to be quite a hassle is the search/search and replace sometimes killing my entire window manager. Were it free software, there’d be a bug report. But for now I work around it. Maybe I’ll drop CLion but I doubt I’ll be writing any Go code in Vim anytime soon.
If you think you have found the perfect IDE or just want to share Vim tips, meet me at the OSMC in November!

Unterschiedliche Datentypen mit boost::variant

In C++ gibt es Fälle, in denen eine Funktion Werte zurückliefern soll, die nicht alle denselben Datentyp besitzen. Ein Beispiel hierfür ist eine Funktion, die einen JSON-String parsen soll und den deserialisierten Wert zurückgeben soll:

<Datentyp?> JsonDeserialize(const std::string& json);

Wenn wir die Funktion in C schreiben würden, könnten wir uns mit einem einfachen Trick behelfen: wir verwenden ein „Tagged Union“ – also ein Union, zu dem wir uns zusätzlich merken, welches Feld darin aktuell gültig ist:

typedef struct {
  int tag; /* Das aktuell gültige Feld in dem Union */
  union {
    double value_double;
    char *value_string;
    ...
  };
} my_variant;

Mit C++ würde dies allerdings nicht funktionieren, da in einem Union nur POD-Typen erlaubt sind – also Datentypen, die keine Konstruktoren, Destruktoren oder andere Methoden enthalten. Der Hintergrund dabei ist, dass C++ nicht entscheiden kann, welchen Konstruktor und Destruktor es für das Union aufrufen soll, da es nicht weiss, welches Feld gültig ist.
Die Boost-Library bietet für dieses Problem eine elegante Lösung an: das boost::variant-Template kann verwendet werden, um eigene Typen zu definieren, die sich wie unser my_variant-Struct verhalten, aber zusätzlich auch nicht-triviale Typen (mit Konstruktor, Destruktor, usw.) enthalten kann:

typedef boost::variant<double, std::string, ...> my_variant;

Der my_variant-Typ verfügt dabei für jeden angegebenen Template-Parameter über einen passenden Konstruktor, der eine einfache Zuweisung erlaubt:

my_variant val = 7;

In diesem Fall wird der interne Typ des Variants auf „double“ gesetzt und der Wert 7 gespeichert.
Um zu prüfen, welchen Typ ein Variant-Wert hat, kann die Methode „which“ verwendet werden. Sie liefert einen null-basierten Index in die Template-Parameter zurück. Mit boost::get kann der Wert eines Variants extrahiert werden:

double dval = boost::get<double>(val);

Alternativ (und von der Boost-Dokumentation vorgeschlagen) kann auch das Visitor-Pattern verwendet werden, um auf die eigentlichen Werte zuzugreifen. Mehr Informationen gibt es in der Boost-Dokumentation.

Code Coverage mit LCOV

Auf der Suche nach einem Tool, das „Code Coverage“-Statistiken für C-/C++-Programme generieren kann, bin ich auf LCOV gestoßen:
lcov-icinga2
LCOV generiert zu GCOV-Coverage-Daten HTML-Seiten, auf denen man sich einen Überblick darüber verschaffen kann, welche Codeteile von Unit Tests nicht abgedeckt werden.
Im Schnelldurchlauf lässt sich LCOV wie folgt verwenden:
1. Das eigene Programm muss mit dem Compiler-Flag „–coverage“ kompiliert werden, z.B.:

$ gcc -o test --coverage test.c

2. Danach entfernen wir zunächst alle alten Coverage-Daten (falls wir unser Programm vorher schonmal ausgeführt haben):

$ lcov -d . -z

3. Anschließend führen wir unser Programm aus:

$ ./test

4. Und abschließend generieren wir HTML-Seiten zum Programmlauf:

$ lcov -d . -c -o test.info
$ genhtml -o lcov-html test.info

Die HTML-Dateien werden dabei in dem mit -o angegebenen Verzeichnis erstellt.

Neuerungen in C++11

C++ ist eine der beliebtesten Programmiersprachen und wird vor allem für komplexe Software-Projekte verwendet, wenn Performance wichtig ist.
Um die Sprache einfacher verständlich und einsatzfähig zu machen, wurde 2011 vom zuständigen ISO-Gremium die neue Version C++11 veröffentlicht, an der bereits seit 2005 gearbeitet wurde.
Eine der meiner Meinung nach wichtigsten Änderungen ist dabei die Unterstützung von Type Inference. Dieses Feature ermöglicht es, Variablen zu deklarieren, ohne ihren genauen Typ angeben zu müssen. Der Compiler erkennt hierbei anhand von dem Initialisierungs-Ausdruck, welchen Typ eine Variabel hat. Dies macht z.B. die Verwendung von STL-Containern einfacher:
Mit C++98:
map::iterator it = m.begin();
Mit C++11:
auto it = m.begin();
Zusammen mit den neuen Range-based For-Loops wird dieses Feature noch mächtiger:
Vorher:

vector::iterator it;
for (it = v.begin(); it != v.end(); it++) {
  ...
}

Nachher:

for (auto i : v)
  ...
}

Die C++-Standard-Library bietet für viele Algorithmen eine Implementation an, z.B. zum Filtern oder Durchsuchen von Listen. Hierfür musste jedoch bisher eine Funktion als Callback übergeben werden, z.B.:

bool compare_numbers(int a, int b)
{
  if (a % 2 == 0 && b % 2 == 0)
    return a < b;
  else
  return (a % 2 == 0);
}
int main(int argc, char **argv)
{
  vector numbers = { 4, 3, 2, 7 };
  sort(numbers.begin(), numbers.end(), compare_numbers);
  for (int i : numbers)
    cout << i << endl;
  return 0;
}

C++11 bringt Unterstützung für Lambda-Ausdrücke, wie sie bereits aus anderen Sprachen wie z.B. Python bekannt sind:

int main(int argc, char **argv)
{
  vector numbers = { 4, 3, 2, 7 };
  sort(numbers.begin(), numbers.end(), [](int a, int b) {
    if (a % 2 == 0 && b % 2 == 0)
      return a < b;
    else
      return (a % 2 == 0);
  });
  for (int i : numbers)
    cout << i << endl;
  return 0;
}

Dies sind nur einige wenige Neuerungen, die den Programmierer-Alltag einfacher machen sollen. Das GNU-Projekt hat eine Liste aller Änderungen in C++11 und deren Implementations-Status in GCC: http://gcc.gnu.org/projects/cxx0x.html
Ein Problem von C++11 ist, dass die meisten Compiler noch nicht alle Features des neuen Standards unterstützen und man gerade bei Open Source-Projekten darauf achten muss, dass auch ältere Compiler-Versionen unterstützt werden. Vermutlich wird es daher noch einige Jahre dauern, bis C++11 allgemeintauglich ist.