Höllische Programmiersprachen Seminar im Wintersemester 2014/15 API als Syntax vs. Bibliotheken Markus Siglreithmaier Technische Universität München Zusammenfassung Diese Ausarbeitung behandelt zwei Implementierungskonzepte der API in unterschiedlichen Programmiersprachen. Hierbei werden die beiden Designkonzepte, das Interface als Teil der Sprachsyntax gegenüber der Auslagerung in eine Bibliothek genauer erläutert. Dazu nach einem kurzen Überblick der verschiedenen Methodiken ein Vergleich hinsichtlich Performanz, Usability und Komplexität der Implementierung. 1 Einführung Eine wichtige Komponente und Qualitätsfaktor einer Sprache stellt die Programmierschnittstelle dar. Daher ergibt sich, dass deren Design und Implementierung ein fundamentaler Bestandteil des Designprozesses für eine Programmiersprache ist. Über den Lauf der Jahre wurden unterschiedliche Ansätze für verschiedene Sprachen und deren jeweiligen Anforderungen entwickelt. Das Verständis für die internen Konzepte ist wichtig zur Bewertung von Performance und Tauglichkeit der Sprache bezüglich der jeweiligen Problemstellung. Dadurch zeigt sich, dass die Betrachtung dieses Themas hohe Relevanz für Programmierer als auch Entwickler und Designer neuer Sprachen aufweist. Betrachten wir dazu als Motivation den Aufruf einer Funktion, welche uns den Sinus eines Wertes berechnet. Dazu vergleichen wir eine exemplarischen Aufruf in C++ std::sin(4.0); mit einem äquivalenten Funktionsaufruf in PL/1 SIN(4); Lässt man die geringfügigen syntaktischen Unterschiede der beiden Sprachen außer Acht, kann man aus Seiten des Programmiereres keinen klaren Unterschied zwischen den beiden Aufrufen erkennen. Jener wird erst beim Übersetzen des 1 Programms sichtbar, wobei es sich im Falle von C++ um eine Bibliotheksfunktion handelt und bei PL/1 um einen built-in Funktion, welche sich in der Art der Übersetzung stark unterscheiden. Der Rest der Arbeit widmet sich dem Vergleich dieser unterschiedlicher Designansätze für APIs gegliedert in die folgenden Abschnitte: • In Kapitel 2.1 wird genauer auf die Definition von APIs eingegangen insbesondere unter Berücksichtigung der Themenstellung. • Eine Einführung in die beiden Designkonzepte der Implementierung der API als Teil der Sprachsyntax (Kapitel 2.2) und als seperate Programmbibliothek (Kapitel 2.3). • Vergleich dieser hinsichtlich mehreren Bewertungsfaktoren für Programmierer als auch unter dem Aspekt des Designs von Programmiersprachen (Kapitel 2.4). • Eine abschließende Bewertung der Ansätze im Bezug auf die vorrangegangenen Vergleichsanalysen erfolgt in Kapitel 3. 2 Sprachkonstrukte vs Bibliothek In diesem Kapitel werden die beiden Designkonzepte für APIs vorgestellt, sowie deren Vorkommen in den jeweiligen Programmiersprachen beschrieben, gefolgt von einem Vergleich dieser bezüglich Performanz, Usability und Komplexität der Implementierung. 2.1 Application Programming Interface Eine API (Abkürzung für Application Programming Interface) beschreibt grundlegend eine Schnittstelle zur Kommunikation mit anderen Systemen und Programmen. Sie stellt die jeweiligen Funktionsdeklerationen bereit, ist jedoch unabhängig von der Implementierung der Funktionen. Betrachetet man Scala standard library, welche auch die artihmentischen Operationen Addition, Multiplikation, etc. explizit implementiert, so kann man auch die Operatoren +,-,/,.. als Teil der Sprach-API betrachen, welche nur ’syntactic sugar’ für die jeweiligen Funktionen darstellen[8]. 2.2 Builtin Funktionen Eine Möglichkeit der Implementierung einer API stellt die Bereitstellung von builtin-functions dar. Diese bezeichnen von der Programmiersprache bereits vordeklarierte Funktionen, welche als keywords dem Programmierer zur Verfügung gestellt werden und somit Teil der Syntax sind. Bekannte Programmiersprachen, welche diese Form von Interface implementieren, sind unter anderem PL/1[6]. 2 Dabei handelt es sich meist um grundlegenden Funktion wie artihmetische Operationen(sin, cos,..) und Zugriff auf Systemfunktionen(IO)[6]. Da die built-in Funktionen Teil der Sprachsyntax sind, werden diese explizit vom Compiler bzw. Interpreter behandelt. Wobei im Falle PL/1 die Funktionsaufrufe direkt in architekturabhängige Maschinenbefehle übersetzt werden[4]. Dieses Konzept findet sich aber auch in neueren Programmiersprachen wie Go wieder, welche häufig verwendete Operation wie map oder append als builtin Funktion implementiert. Zudem haben die Entwickler, als weiteres Beispiel, ein go-statement implementiert, welcher zur flexiblen und einfach Erzeugung von asynchronen Routinen dient[2]. 2.3 Programmbibliothek Eine Programmbibliothek (engl. library) bezeichnet ein Modul, welches Funktionen, Teilprogramme und andere Konstrukte beinhaltet, welches wiederum von anderen Programmen verwendet werden kann. Dieses Konzept ist weit verbreitet und findet sich in vielen Programmiersprachen wieder. Einige prominente Vertreter sind unter anderem Haskell und C++. Da die Funktionsdekleration oft unabhängig von der Definition ist, wird ein zusätzlicher Schritt beim Compileprozess benötigt, um diese miteinandere zu verbinden. Dieser Vorgang wird als Linking bezeichnet und kann auf unterschiedlichen Arten durchgeführt werden[9]. 2.3.1 Static Linking Bei statischen Bibliotheken handelt es sich um Bibliotheken welche beim Linking in das Endprogramm statisch eingebunden werden. Dabei kopiert der Linker entweder die komplette Bibliothek oder auch nur benötigte Teilkomponenten ans Ende des Programmcodes[10]. Dadurch steigt die Größe des kompilierten Programms um die Größe der Bibliothek. Durch das statische Einbinden ist das Programm jedoch nicht auf eine systemeigene Bibliothek mehr angwiesen, da die jeweilige library schon mitgeliefert, was die Portabilität erhöht und unnötige Versionskonflikte vermeidet. 2.3.2 Dynamic Linking Bei dynamischen Bibliotheken werden, im Gegensatz zu statischen, werden die Modulkomponenten der jeweiligen Bibliothek erst zur Laufzeit des Programms in den Arbeitsspeicher geladen und stehen dann erst dem Programm zur Verfügung[10]. Indem das Betriebssystem die Bibliothek in den virtuellen Speicher lädt, unabhängig von dem jeweiligen Programm, können auch andere Prozesse parallel auf dieselbe Bibliothek zugreifen, wodurch Systemressourcen gespart werden können. Dies ist vorallem interressant für systemeigene Bibliotheken, da diese nur einmal geladen werden müssen und nicht die Größe des Programms negativ beeinflussen[3]. Durch das dynamische Laden der Bibliothek ist lediglich eine Kompitibiltät der API für das Programm notwendig, 3 sodass die Bibliothek unabhängig davon verändert werden kann. In der Praxis kann dies jedoch zu Konflikten kommen durch Abhängigkeit an unterschiedlichen Versionen. Gleichzeitig bietet dies aber auch eine einfache Möglichkeit des Programmupdates im Gegensatz zum statischen Linking. 2.4 2.4.1 Vergleich der beiden Ansätze Peformance In diesem Kapitel wird genauer die Performanz der beiden Konzepte analysiert, was vor allem für Softwareentwickler interessant ist. Wenn eine Programmiersprache die jeweilige Funktion als Teil der Syntax implementiert ergeben sich mehrere Optimierungsmöglichkeiten für den Compiler. Sie erlaubt eine optimierte Übersetzung in Maschinensprache, welches vor allem in instruction count abhängigen Arbeitsfeldern, z.B. GPU-Shadern, von Vorteil ist. So bieten Shadersprachen wie HLSL eine Vielzahl von built-in Funktionen an zur Performanzverbesserung[5]. Dahingegen benötigt man bei Bibliotheken einen zusätlichen Funktionsaufruf, welcher abhängig von der Art des Linking mit zusätzlichen Performanzkosten verbunden sein kann (dynamic linking). Bei einige Sprachen wie C++ kann dies vom Compiler durch Inlining der Funktion kompensiert werden. Zudem wurden im Laufe der Zeit weitere Möglichkeiten zur Optimierung der Perfomanz entwickelt, unter anderem Link-Time-Optimization des GNU C Compiler. Dabei wird das Programm erst beim Linken im Gegensatz zur klassischen Method beim kompilieren der .obj-Datei, was dem Compiler mehr Optionen lässt[7]. Daraus zeigt sich, dass Bibliotheksfunktionen im den meisten Fällen eine ähnliche Performanz bieten wie built-in Funktionen, da außerdem die Geschwindigkeit sehr stark von der jeweiligen Funktionsimplementierung abhängt. 2.4.2 Usability Beim Arbeiten mit einer Programmiersprache ist es wichtig, dass sie intuitiv und leicht benutzbar ist, um einen optimalen Workflow zu erreichen. Die Variante über die Syntax bietet durch die Unterstützung seitens des Compilers eine Vorteil im Gegensatz zu Bibliotheksfunktionen. Da dem Compiler die Funktion und der Implementierung bekannt ist, können zusätzliche Überprüfungen zur Compiletime gemacht werden. Dies dient zur Prävention von Bugs und erleichtert die Wartung für den Programmierer. Hierbei illustriert Eberhard Sturm in seinem Artikel ’Power vs. Adventure - PL/I and C’ ein gutes Beispiel für Type Checking des Compilers in PL/1[11]: Er vergleicht die Verarbeitung der Formatierungshinweise der C-Funktion printf mit dem Äquivalent put edit aus PL/1. Hierbei sieht man, dass C keine Möglichkeit der Validierung der Parameter zulässt, da diese erst zur Laufzeit ausgewertet werden, im Gegensatz zu PL/1, wo deren Typ bereits zur Übersetzungszeit betrachtet wird. Ein weiterer Vorteil stellt eine mögliche Verbesserung der Lesbarkeit des Codes dar. Durch Einfügen in die Keywordliste besteht die Option viel genutzte Funktionalitäten durch eine kompakte Syntax darzustellen, wie es am Beispiel der 4 go-Routinen sichtbar wird. Ein weiteres Beispiel SQL, womit mittels einer problemorientierte Syntax ohne zustäzlichem Einbinden von Bibliotheken effizient Datenbankanfragen generiert werden können. Bibliotheken hingegen glänzen durch Flexibilität und Austauschbarkeit, vor allem wenn dynamisch gebunden. Unterschiedliche Implementierungen können einfach gewechselt werden durch linken einer anderen API-konformeen Bibliothek bei statischen Bibliotheken oder nur durch Laden einer neuen dynamischen Bibliothek. Dies ermöglich z.B. das einfache Logging von Funktionsaufrufen, welches beim Debuggen von OpenGL-Calls verbreitet ist[12]. Hierbei kann es mit Bibliotheken zu der bereits erwähnten Problematik des Versionings kommen, wo es zu Konflikten durch unterschiedliche Versionsnummern kommen kann. 2.4.3 Implementierung Wenn die API als Teil der Syntax implementiert ist, gibt es folglich eine hohe Verflechtung zwischen Compiler und API, da die Funktionen der API keywords sind, welche vom Compiler unterstützt werden müssen. Dies führt zu einer vergrößerten Komplexität hinsichtlich Wartung und Implementierung beim Compilerbau für diese Sprache, da zusätzlicher Aufwand im Parsing und Übersetzen geleistet werden muss, was sich negativ auf die Größe des Compilers auswirkt. Für den Programmierer bedeutet dies, dass bei Änderungen der API auch ein Update des Compilers anfallen können. Bei Bibliotheken wird, wie oben angesprochen, ein zusätzlicher Prozess beim Kompilieren benötigt. Implementierung des Linkers und Loaders bedeutet zusätzliche Mehrkosten, welche jedoch unabhängig von der jeweiligen API sind. Dies führt zu einer besseren Entkopplung der Komponenten im Vergleich zu dem API-Support mittels Keywords. 3 Bewertung Vergleicht man die beiden Konzepte anhand der vorrangegangenen Analyse sieht man, dass jedes für sich Vor- und Nachteile in den einzelnen Bereichen vorweisen kann. Deshalb ist es wichtig bei der Wahl des jeweiligen Designs genau die Problemkategorie, welche durch die Programmiersprache gelöst werden soll, zu betrachten. Untersucht man die derzeitigen Trends und Entwicklung von Programmiersprachen so wird die API überwiegend durch Bibliotheksfunktionen in den ’moderneren’ Sprachen wie Scala, Rust, etc., bzw. eine Mischung aus beiden Konzepten (siehe Go). Dies lässt sich auf ein modulares Design der Sprachen zurückführen, welche eine relativ große Standard-API besitzen und in einzelne Komponenten aufgeteilt wird[1]. Das Argument einer möglichen höheren Performance durch Syntax-Support scheint nur in bestimmten Problembereichen wie zum Beispiel der angesprochenen Shaderprogrammierung zum Tragen zu kommen. 5 Literatur [1] http://doc.rust-lang.org/0.12.0/std/. [2] The Go Programming Language Specification. https://golang.org/ref/spec. [3] Vorteile der Verwendung von DLLs. de/library/dtba4t8b.aspx. http://msdn.microsoft.com/de- [4] Enterprise PL/I for z/OS: Enterprise PL/I Language Reference, 2003. Fifth Edition. [5] Reference for HLSL: Intrinsic Functions, 2014. http://msdn.microsoft.com/en-us/library/windows/desktop/ff471376 [6] Paul Abrahams. The PL/I Programming Language. 3 1978. [7] T. Glek and J. Hubicka. Optimizing real world applications with GCC link time optimization. CoRR, abs/1010.2196, 2010. [8] M. Odersky, S. Micheloud, N. Mihaylov, M. Schinz, E. Stenman, M. Zenger, and et al. An overview of the scala programming language. Technical report, 2004. [9] L. Presser and J.R. White. Linkers and loaders. In ACM Computing Surveys, volume 4, Nr. 3, pages 149–167. 9 1972. [10] Michael L. Scott. Programming Language Pragmatics. Morgan Kaufmann Pusblishers, Inc., 2000. [11] Eberhard Sturm. Power vs. Adventure - PL/I and C. 10 1994. [12] Damian Trebilco. https://code.google.com/p/glintercept/. 6