FrançaisEnglish

5 · Runtime & interopérabilité C

Modèle mémoire

Amalgame utilise bdwgc (le ramasse-miettes Boehm-Demers-Weiser) pour toutes les allocations dynamiques. Le GC est un collecteur conservateur mark-sweep qui réside dans runtime/_runtime.h (#include <gc.h>), et est initialisé par le int main() généré :

int main(int argc, char** argv) {
    GC_INIT();
    code_runtime_init_args(argc, argv);
    App_Program_Main((code_string*)argv);
    return code_exit_code;
}

Implications :

Correspondance des types (Amalgame → C)

Amalgame C
int i64 (signé 64 bits)
float double
bool code_bool (alias de bool)
string code_string (alias de char*)
void void
T? T* (pointeur nullable)
T[] T* (le tableau se dégrade en pointeur ; taille fournie par l'appelant)
List<T> AmalgameList*
Map<K,V> AmalgameMap*
Set<T> AmalgameSet*
class Foo utilisateur Foo* (toujours alloué sur le tas)
enum Foo simple Foo (enum C) — non-pointeur
enum Foo algébrique Foo (struct union taguée) — type valeur

Les paramètres génériques (T) sont effacés en void*. Les valeurs primitives (int, bool) sont transmises via des casts (void*)(intptr_t) lors des passages aux interfaces génériques — il n'existe pas d'objet de boxing.

Les noms de symboles C sont dérivés de Namespace.Class.MethodNamespace_Class_Method. Les méthodes statiques et les méthodes d'instance partagent le même schéma de nommage ; les méthodes d'instance prennent self comme premier paramètre.

Closures et appels d'ordre supérieur

Une lambda est compilée en une fonction C de premier niveau plus une valeur AmalgameClosure { fn, env }. _runtime.h expose la structure et trois wrappers d'appel :

AmalgameClosure_call1(c, arg)
AmalgameClosure_call2(c, a, b)
AmalgameClosure_call3(c, a, b, d)

Les méthodes d'ordre supérieur de List<T> (Filter, Map, Reduce, ForEach, Any, All, CountIf) sont implémentées sous forme d'inlines statiques dans _runtime.h qui dispatchent via _call1 (ou _call2 pour le réducteur (acc, x) → acc de Reduce). Au niveau du site d'appel Amalgame, le CGen émet une compound-statement-expression GCC qui alloue l'environnement, y copie les locaux capturés, et produit une closure fraîche :

AmalgameList_filter(xs, ({
    LamEnv_0* __env_0 = (LamEnv_0*)code_alloc(sizeof(LamEnv_0));
    __env_0->_n = n;             /* one line per capture */
    AmalgameClosure_new((void*)lam_0_fn, __env_0);
}))

Les éléments, arguments et résultats des lambdas sont tous des void* boxés à la frontière de l'ABI C ; le site d'appel déboxe via (i64)(intptr_t)… pour la forme i64. Les signatures non-int (p. ex. une lambda retournant un code_string) nécessitent la couche de typage de lambda qui est prévue pour la prochaine version.

Appeler Amalgame depuis C

Compilez votre bibliothèque avec --lib et liez le .o :

// greeter.am
namespace MyLib

public class Greeter {
    public Name: string
    public Greeter(string name) { this.Name = name }
    public string Hello() { return "Hello, " + this.Name + "!" }
}
amc --lib greeter.am -o greeter
gcc -Iruntime -c greeter.c -o greeter.o

Consommateur C :

#include "_runtime.h"
#include "Amalgame_String.h"

typedef struct _MyLib_Greeter MyLib_Greeter;
extern MyLib_Greeter* MyLib_Greeter_new(code_string name);
extern code_string MyLib_Greeter_Hello(MyLib_Greeter* self);

int main(void) {
    code_runtime_init();
    MyLib_Greeter* g = MyLib_Greeter_new("World");
    printf("%s\n", MyLib_Greeter_Hello(g));
    return 0;
}
gcc -Iruntime consumer.c greeter.o -lgc -lm -o app
./app
# Hello, World!

Un test e2e opérationnel se trouve dans tests/samples/lib_e2e.am / lib_e2e_consumer.c et s'exécute dans le cadre de tests/core_bundle/core_test.am (invoquez avec ./amc test ./tests/core_bundle/).

Appeler C depuis Amalgame

Il n'existe pas de mot-clé FFI aujourd'hui. Deux approches pragmatiques :

1. Ajouter la fonction au header runtime

Déposez votre déclaration dans un header sous runtime/ et déclarez-la comme builtin dans le resolver :

// runtime/Amalgame_String.h
static inline i64 String_DamerauLevenshtein(code_string a, code_string b) {
    /* … your impl … */
}
// src/resolver/resolver.am — RegisterBuiltins()
this.DeclareGlobal("String_DamerauLevenshtein", "int", false)

Vous pouvez désormais appeler String.DamerauLevenshtein(a, b) depuis n'importe quel fichier .am.

C'est exactement ainsi que fonctionne la stdlib en interne.

2. Inliner un site d'appel C via le corps d'une méthode

Moins élégant, mais fonctionnel pour des bindings ponctuels : placez l'utilitaire dans le header runtime, puis écrivez une fine classe Amalgame qui ne fait que transférer. Le CGen émettra des appels C normaux.

3. Blocs C inline — @c { … } (v0.7.4+)

Depuis la v0.7.4 (projet G), Amalgame dispose d'un bloc C inline balisé qui permet au corps d'une méthode de plonger directement en C sans passer par un détour de header runtime. Les octets du corps traversent le lexer de manière opaque jusqu'au } correspondant, le resolver / typechecker / linter traitent le nœud comme opaque, et le cgen splice la source verbatim à l'intérieur d'une instruction composée ({ … }) afin que toute variable locale déclarée dans le bloc reste limitée à sa portée.

namespace App

public class CTools {
    public static int CLen(string s) {
        @c {
            return (int) strlen(s);
        }
    }

    public static int Doubled(int x) {
        @c {
            int local = x;
            return local + local;
        }
    }
}

Deux directives de portée fichier complémentaires sont reconnues en tête de tout fichier .am, avant ou entre les déclarations de classes :

Sémantique du corps :

Sandbox / sécurité : il n'y en a pas. Le C inline peut planter le programme, provoquer des fuites mémoire et violer les invariants du GC. Traitez @c { … } comme vous traiteriez unsafe { … } en Rust — utilisez-le quand vous n'avez pas le choix, documentez pourquoi, et isolez la surface non sécurisée derrière un wrapper Amalgame typé.

Blocs @c { … } de portée fichier (v0.7.5+). Une seconde forme de @c { … } réside au niveau supérieur d'un fichier .am (entre ou avant les déclarations de classes). Le corps est splicé dans le .c émis en dehors de toute fonction, ce qui permet à un module de :

namespace Amalgame.Logging

@c {
    #include <stdio.h>
    static int g_min_level = 1;
    static FILE *g_sink = NULL;
}

public class Log {
    public static void SetMinLevel(s: string) {
        @c { g_min_level = parse_level(s); }
    }
    // … etc.
}

C'est la fonctionnalité clé qui a permis l'arc de pureté de la stdlib en v0.7.6 : les headers runtime sont passés de 18 à 9 tandis que huit modules — Logging, DateTime, FileWatcher, Service, Random, Crypto, BuildInfo, Math + Math.Vec — migraient vers des modules purement AM avec des blocs @c { … } pour les primitives liées à l'OS. La v0.7.7 a ensuite déplacé ces mêmes modules hors de la stdlib embarquée vers des packages externes sur amalgame-lang/ — voir le tableau au chapitre 4.

@out = expr; issu de la proposition originale v0.7.4 a été supprimé. Les corps utilisent le return …; C ordinaire vis-à-vis du type de retour déclaré de la méthode, ce qui est plus propre et évite une réécriture de frontière cachée.

Notes ABI

Chaînes : zéro coût caché

code_string est un char*. La concaténation passe par code_string_concat(a, b) qui alloue un nouveau tampon GC de strlen(a) + strlen(b) + 1. Passer un littéral est gratuit.

Il n'y a pas de cache de longueur — String.Length(s) appelle strlen(s) — donc les boucles chaudes qui demandent la longueur devraient la hisser hors de la boucle :

let len = String.Length(text)         // O(strlen)
for i in 0..len { /* … */ }            // corps en O(1) par itération

Threading & async — packages externes

Le runtime embarqué est mono-thread par défaut ; le GC est configuré pour le thread principal uniquement. Deux packages de l'écosystème prennent en charge la concurrence pour vous éviter d'écrire des blocs @c { … } manuels autour de pthread / ucontext :

Ils se composent de manière orthogonale (spawner N threads OS, faire tourner un scheduler dans chacun — M:N est prévu pour amalgame-async v0.4). Si vous forkez ou spawner des threads POSIX depuis vos propres blocs @c { … } sans passer par l'un de ces packages, vous êtes seul responsable de la sécurité vis-à-vis de libgc : le wrapper GC_pthread_create est ce qui tient le collecteur informé des piles des nouveaux threads.