APP-Notes

Dag 8 - Interfacing with C

Omdat het de bedoeling is dat wij in het ASD project gaan werken met pluginsz, is het goed om eens te expirimenteren met dit soort technieken. Joost gaf ons het advies om eens te kijken naar de C-interface van verschillende programmeertalen. Toevallig gaat er in mijn Haskell boek ook een hoofdstuk over dit onderwerp, dus dit lijkt mij de perfecte kans om hier eens wat onderzoek naar te doen.

Haskell naar C

De Haskell Foreign Function Interface (FFI) wordt gebruikt om te communiceren met andere programmeertalen, zoals C. Om dit voor elkaar te krijgen, moet de FFI functionaliteit eerst aangezet worden. Dit is te doen door de volgende regel bovenaan de .hs sourcefile te zetten:

{-# LANGUAGE ForeignFunctionInterface #-}

import Foreign
import Foreign.C.Types

Om vervolgens de C functie ‘sin’ aan te roepen die gedefinieerd staat in math.h, gebruiken we een foreign import:

foreign import ccall "math.h sin"
    c_sin :: CDouble -> CDouble

C sin

Omdat er gecommuniceerd wordt met C, kan de puurheid van functies niet gewaarborgd worden. Ook heb je als developer zelf in de hand welke parameters er aan de C functie meegegeven worden. Haskell dwingt niet af dat de goede types gedeclareerd worden. Dit kan dus resulteren in een runtime error wanneer de parameters niet kloppen.

Type conversie

Wanneer vanuit Haskell C functies aangeroepen worden, wordt er gebruik gemaakt van C-specifieke data types. Om deze te converteren naar Haskell types en weer terug, bestaan er een aantal functies. Voorbeeldne hiervan zijn realToFrac of fromIntegral. realToFrac dient voor het converteren van verschillende floating-point nummers. fromIntegralconverteert integer waardes. Voor andere datatypes zijn er soortgelijke functies of methodes voor conversie.

C naar Haskell

Om Haskell aan te roepen vanuit C, moet ik eerst weten hoe ik uberhaupt een C programma kan uitvoeren. Na wat rond gegoogled te hebben, ben ik erachter gekomen dat de commandline tool gcc gebruikt wordt voor het compileren van C programma’s. Daarnaast zie ik dat C bestanden de .c extentie hebben. Om te testen of dit werkt, heb ik het volgende stukje C in het bestand tohaskell.c opgeslagen. Eerst importeer ik de stdio.h library. Deze library maakt het mogelijk om iets naar de console te schrijven.

#include <stdio.h>

int main()
{
    printf("test");
}

Om dit stukje C vervolgens te compilen en te runnen, gebruik ik het command gcc -o tohaskell tohaskell.c. Dit compileert het stukje C en maakt een executable aan genaamd tohaskell. Deze kan gedraaid worden door simpelweg het bestand in de commandline uit te voeren als ./tohaskell.

Simple C

Nu we een C applicatie kunnen draaien, hebben we een Haskell bestand nodig waar de uit te voeren functies in gedefinieerd staan. Omdat het boek dit niet dekt, ben ik gaan zoeken op internet. Ik ben uiteindelijk op dit artikel terecht gekomen. Hierin wordt een fibbonaci functie beschreven. Deze neem ik over. De file waar ik dit in stop heet fromc.hs. In deze code is te zien dat de ForeignFunctionInterface (FFI) weer toegevoegd wordt. Vervolgens hebben we weer een foreign export ccall die voor de interfacing tussen C en Haskell zorgt. Voor de fibonnaci_hs functie is het een kwestie van het parsen van CInt naar Int, het aanroepen van de fibonacci functie en vervolgens weer het terugcasten.

{-# LANGUAGE ForeignFunctionInterface #-}

module FromC where
    
import Foreign.C.Types

fibonacci :: Int -> Int
fibonacci n = fibs !! n
    where fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
    
fibonacci_hs :: CInt -> CInt
fibonacci_hs = fromIntegral . fibonacci . fromIntegral
    
foreign export ccall fibonacci_hs :: CInt -> CInt

Om dit stukje Haskell vervolgens aan te spreken, hebben we een stukje C code nodig. het artikel beschrijft het volgende code stukje. In dit stukje is te zien, dat FFI geinclude wordt. Daarnaast wordt het gecompileerde stukje Haskell geinclude. Vervolgens roepen we de fibonacci functie aan met het argument 42.

#include <HsFFI.h>
#ifdef __GLASGOW_HASKELL__
#include "fromc_stub.h"
extern void __stginit_FromC(void);
#endif
#include <stdio.h>

int main(int argc, char *argv[])
{
    int i;
    hs_init(&argc, &argv);
#ifdef __GLASGOW_HASKELL__
    hs_add_root(__stginit_FromC);
#endif

    i = fibonacci_hs(42);
    printf("Fibonacci: %d\n", i);

    hs_exit();
    return 0;
}

Om dit nu te compileren, zijn er 2 commands nodig. Eerst moet de Haskell code gecompileerd worden met het command ghc -c -O fromc.hs.

Vervolgens kunnen we de C code compileren met het command ghc --make -no-hs-main -optc-O test.c FromC -o test. Dit genereert een gecompileerde c-file met de naam test die we nu weer simpelweg kunnen runnen via de commandline. Als we dit doen, zien we het resultaat van de fubonnaci functie.

Fibonacci C

Java naar C

Via dit artikel ben ik te weten gekomen hoe ik C aan kan spreken vanuit java.

Als eerst hebben we een java file nodig om C aan te kunnen spreken. In deze java file laden we de C-library genaamd ‘HelloWorld’. Daarnaast declareren we een native void genaamd print. Deze native void correspondeert met de C functie.

class JavaCall {
    private native void print();

    public static void main(String[] args) {
        new JavaCall().print();
    }

    static {
        System.loadLibrary("JavaCall");
    }
}

De volgende stap is het compileren van het java bestand met het command javac javacall.java. Dit genereert de file JavaCall.class.

Vervolgens hebben we een C-header file nodig. Deze genereren we op basis van de hiervoor geschreven java code. Hiervoor is het volgende command: javah -jni JavaCall. Dit genereert de file JavaCall.h. Deze file heeft de volgende content. Hier zien we dat er wat java-specifieke dingen geinclude worden. Daarnaast zien we de print functie terugkomen. Wat opvalt, is dat deze functie 2 argumenten heeft. Dit zijn java-specifieke argumenten.

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class JavaCall */

#ifndef _Included_JavaCall
#define _Included_JavaCall
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     JavaCall
 * Method:    print
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_JavaCall_print
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

Header files dienen als een soort interface. Deze interface kunnen we implementeren met een .c bestand

#include <jni.h>
#include <stdio.h>
#include "HelloWorld.h"

JNIEXPORT void JNICALL Java_JavaCall_print(JNIEnv *env, jobject obj)  {
     printf("Hello World!\n");
     return;
}

Om de C files te compileren, gebruiken we de cc tool. Meer MacOS-specifieke uitleg heb ik hier gevonden. Deze uitleg dekt geen mac-specifieke cases. Voor mac-specifieke commands, heb ik deze blogpost na veel trial-and-error gevonden.

Compileren doe ik met het command cc -c -I/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/include/-I/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/include/darwin/ JavaCall.c. Dit genereert het bestand JavaCall.o.

Vervolgens moet ik hier een native library van maken, zodat deze gebruikt kan worden in java. Hiervoor gebruik ik het command cc -dynamiclib -o libjavacall.jnilib JavaCall.o -framework JavaVM. Dit genereert, zoals het command al doet vermoeden, het bestand libjavacall.jnilib.

Wanneer ik vervolgens de java code run met het command java JavaCall, krijg ik de volgende output:

JavaCall output

Java naar Haskell

Om te kunnen praten van Java naar Haskell, wil ik de voorgaande voorbeelden aan elkaar knopen. Na hier 3 uur in gestopt te hebben, ben ik een eind gekomen. Het is echter niet gelukt.

Ik heb uitgevonden dat het C -> Haskell voorbeeld, die nu met de ghc compiler gecompileerd wordt, ook prima met de cc compiler gecompileerd kan worden. Omdat ik niet veel kon vinden over het compilen van C met de ghc compiler, leek mij het een goede stop om dit eerst ook via de cc compiler te kunnen. Wat hierbij belangrijk is, is de file ‘HsFFI.h’. .h files zijn, zoals eerder benoemd interfaces. HsFFI is nodig voor het compileren van de C code die met Haskell gaat praten. In de ghc compiler wordt deze standaard meegeleverd. Echter is dit niet het geval wanneer er gecompileerd wordt via de cc compiler. Na wat speurwerk ben ik erachter gekomen dat hiervoor de -I parameter gebruikt wordt. Ook duurde het even voordat ik de folder gevonden had waar de HsFFI.h file verstopt zat. Nu ik deze gevonden had, kon ik de C->Haskell code compileren. Eerst heb ik de test.c file hernoemd naar HaskellCall.c en de bijbehorende .h file naar HaskellCall.h. Het compileren kon ik vervolgens met cc via het command cc -c -I/Library/Frameworks/GHC.framework/Versions/8.2.2-x86_64/usr/lib/ghc-8.2.2/include/ HaskellCall.c. Dit genereert de file HakellCall.o die, zoals verwacht` wederom gebruikt kan worden om Haskell aan te spreken.

Om ervoor te zorgen dat HaskellCall.c geen console applicatie meer is, maar een library, heb ik de functies wat omgebouwd. Voorheen werd de funcite hs_init aangeroepen met argumenten uit de main functie. Deze heb ik nu expliciet als 0 en een lege array meegegeven en dit werkt als gedacht. Deze file is nu als volgt:

#include <HsFFI.h>
#ifdef __GLASGOW_HASKELL__
#include "fromc_stub.h"
extern void __stginit_FromC(void);
#endif
#include <stdio.h>

int invokeFib(int fib)
{
    int i;
    char arr[0];
    
    hs_init(0, arr);
    #ifdef __GLASGOW_HASKELL__
        hs_add_root(__stginit_FromC);
    #endif

    i = fibonacci_hs(fib);

    hs_exit();
    return i;
}

Vervolgens heb ik geprobeerd om via een include de HaskellCall file te includen in de JavaCall.c file. Vervolgens kon ik dit gebruiken om de invokeFib aan te roepen.

#include <jni.h>
#include <stdio.h>
#include "JavaCall.h"
#include "HaskellCall.h"

JNIEXPORT void JNICALL Java_JavaCall_print(JNIEnv *env, jobject obj) {
    int i = invokeFib(42);

    printf("Fibonacci: %d\n", i);
    return;
}

Deze file wordt op dezelfde manier gecompileerd als voorheen. Via cc -c -I/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/include/ -I/Library/Java/JavaVirtualMachines/jdk1.8.0_131.jdk/Contents/Home/include/darwin/ JavaCall.c.

So far so good. Alles lijkt goed te gaan. Op een enkele compiler warning na betreffende mijn code kwaliteit, lijkt alles goed te zijn gegaan. De betreffende .o files zijn gegenereerd.

In het vorige hoofdstuk zagen we dat we vervolgens een library file konden maken van de gecompileerde C code. Hiervoor gebruikte we het command cc -dynamiclib -o libjavacall.jnilib JavaCall.o -framework JavaVM. Dit is waar het fout gaat. Wanneer deze command uitgevoerd wordt, komt de volgende output in beeld:

javalib

‘symbol(s) not found for architecture x86_64’. Het lijkt erop dat de gelinkte files niet aanwezig zijn of niet gevonden kunnen worden. Na wat speurwerk, kwam ik erachter dat er in C files aan elkaar gelinkt worden. Wanneer de ene file een referentie naar de andere file heeft, worden die samengevoegd in de executable. Vervolgens kwam ik erachter dat de -c parameter in het command voor het compilen van JavaCall.c ervoor zorgt dat dingen juist niet gelinkt worden. Het weghalen van deze parameter levert nog meer foutmeldingen op. Foutmeldingen die vervolgens zeggen dat de haskell-specifieke functies uit HaskellCall.c niet meer gevonden kunnen worden. Het weghalen van de -c voor het compileren van de HaskellCall.c levert nog meer foutmeldingen op.

Om dit op te lossen heb ik geprobeerd om de twee bestanden in 1 keer te compileren, met en zonder -c parameter. Beide opties hebben geen succes gehad. Tijdens een lange zoektocht en het uitpluizen van de algemene werking van de includes ben ik er nog steeds niet uitgekomen. Ik ben ten einde raad en ik heb besloten om mijn blog, met toevoeging van deze verklaring, in te leveren.

Bedankt voor het lezen! Zoals joost zou zeggen: “Daarmee is de kous af!”.