C's flexibele integer-groottes waren geen ontwerpfout

Bij onze school werken we met veel retro-machines en oude spelconsoles, dus het is geen verrassing dat wij vaak het eerste aanspreekpunt zijn voor veel ontwikkelaars die de programmeertaal C leren. Veel van onze studenten komen uit talen als Python, JavaScript, Java, C#, Swift en anderen.

Het duurt niet lang voordat beginners beseffen dat veel aspecten die ze in hun primaire taal als vanzelfsprekend beschouwden, in C niet noodzakelijkerwijs gegarandeerd zijn. Vaak krijgen we vragen als:

  • Hoeveel bits neemt deze variabele in het geheugen in beslag?
  • Wat is de padding in deze structuur?
  • Is dit veld correct uitgelijnd in het RAM?

Integers zijn geen 32-bits

Een andere rite de passage voor C-beginners is het leren gebruiken en begrijpen van de operator sizeof.

Wanneer ze in hun programma een 4 zien verschijnen bij het aanvragen van sizeof(int), lijkt dat redelijk, aangezien een signed integer op de meeste moderne machines inderdaad 4 bytes in het geheugen inneemt.

sizeof int: 4 bytes

Helaas moet ik mijn studenten ook vertellen dat toen ik voor het eerst begon met coderen, mijn oude 386-machine 2 bytes zou uitsturen als ik vroeg om de sizeof(int).

sizeof int: 2 bytes

Op dit punt beginnen we een discussie over het feit dat native C-integer-types geen vaste grootte hebben. Talen zoals char, int, short en long komen niet met een garantie over hoeveel bytes ze in het geheugen innemen.

Omdat we in onze code meestal vaste groottes willen, adviseren we studenten om de header STDINT.H te gebruiken om gebruik te maken van daadwerkelijk vaste-grootte types, zoals:

  • int8_t: signed integers van precies 8 bits
  • uint8_t: unsigned integers van precies 8 bits
  • int16_t: signed integers van precies 16 bits
  • uint16_t: unsigned integers van precies 16 bits
  • int32_t: signed integers van precies 32 bits
  • uint32_t: unsigned integers van precies 32 bits
  • etc.

De header <STDINT.H> werd geïntroduceerd als onderdeel van de C99-standaard (gepubliceerd in 1999). Hiermee kunnen programmeurs garanderen dat hun variabelen een exact aantal bytes innemen, ongeacht de compiler en de machine-architectuur waarvoor wordt gecompileerd.

Zijn niet-vaste integer-groottes een ontwerpfout?

Een vrij gangbare opvatting in programmeerkringen is dat de platformafhankelijke integer-types van C een ontwerpfout waren.

Om eerlijk te zijn, dat is niet onterecht. De meeste programmeurs die C gebruiken op moderne systemen zouden precies dat beargumenteren. Een int is 16 bits op de ene machine en 32 op een andere; long is 64 bits op Linux maar 32 op 64-bit Windows. Dit heeft decennia aan portability-bugs veroorzaakt. Dus, gezien het feit dat tegenwoordig bijna elke CPU een 64-bit machine is die 8-, 16-, 32- en 64-bit rekenkunde afhandelt... waarom heeft C de groottes niet vanaf het begin vastgelegd?

In feite proberen we hier een ontwerpbeslissing uit de jaren 70 te beoordelen aan de hand van omstandigheden uit de jaren 2020. C was gebouwd om een draagbare systeemprogrammeertaal te zijn die efficiënt kon worden afgebeeld op zeer verschillende computerarchitecturen. De flexibele type-groottes waren geen overzicht; ze waren de manier waarop C die draagbaarheid bereikte.

Houd er rekening mee dat het doel van "draagbaarheid" voor C iets anders betekende dan de belofte van Java: "schrijf één keer, draai overal". Om te begrijpen waarom, moet je onthouden wat "een computer" betekende toen C werd ontworpen.

De wereld voordat de 8-bits byte won

Tegenwoordig nemen we veel dingen voor lief. Bijna elke machine die je aanraakt heeft:

  • 8-bits bytes
  • Byte-adresseerbaar geheugen
  • 32- of 64-bits registers
  • Two's-complement integers
  • Een vlak, conventioneel geheugenmodel

Niets hiervan was gegarandeerd in de jaren 60 en 70. De industrie was nog niet geconvergeerd en de woordgroottes van machines varieerden enorm:

MachineWoordgrootteNotities
DEC PDP-812 bitsZeer populaire minicomputer
DEC PDP-718 bitsWaar UNIX werd geboren, in assembly
DEC PDP-1116 bitsByte-adresseerbaar; waar C groeide
DEC PDP-10 / DECSYSTEM-2036 bitsKarakters werden vaak 7 of 9 bits tegelijk gepakt
Honeywell 6000 serie36 bits9-bit karakters; een vroeg C-doelwit
UNIVAC 1100 / Unisys 220036 bitsOnes'-complement rekenkunde, 9-bit chars; heeft vandaag nog steeds een C-compiler
IBM 7090 / 709436 bits6-bit karaktercodes
SDS 940, ICL 1900, Harris24 bitsICL gebruikte 6-bit karakters
Burroughs B5000 familie48 bitsTagged, stack-georiënteerde architectuur
CDC 660060 bits6-bit karakters, geen byte-adressering
Cray-164 bitsWoord-adresseerbaar; in C konden short, int en long allemaal 64 bits zijn
Data General Nova16 bitsWoord-adresseerbaar; byte-pointers hadden een andere representatie dan woord-pointers
Intel 808616 bitsGesegmenteerd geheugen; near en far pointers

Karakters waren ook niet consistent. Er waren 6-bit karaktersets, 7-bit ASCII, 9-bit bytes op 36-bit machines en EBCDIC op IBM mainframes. Negatieve getallen konden worden opgeslagen als two's complement, ones' complement of sign-magnitude. Sommige machines konden individuele bytes adresseren. Andere konden alleen hele woorden adresseren, waardoor een "pointer naar een karakter" een woordadres plus een offset moest zijn.

Een taal die zowel efficiënt als draagbaar wilde zijn over deze machines, kon weinig aannemen.

Waar de types van C vandaan kwamen

De integer-filosofie van C wordt logischer als je naar de voorlopers kijkt.

BCPL (Martin Richards, 1967) en B (Ken Thompson, rond 1969) waren typeloze talen. Er was slechts één soort waarde: het machine-woord. Een variabele bevatte een woord. Je kon het behandelen als een integer, een adres of een bitpatroon, afhankelijk van de operator die je toepaste. Op de woord-adresseerbare machines waar deze talen op waren gericht, was dat elegant en efficiënt.

Toen kreeg Bell Labs een PDP-11. De PDP-11 was byte-adresseerbaar, met 16-bit woorden, en stond op het punt floating-point hardware te krijgen. Dennis Ritchie beschrijft in The Development of the C Language (1993) hoe slecht het "alles is een woord"-model van B paste op die machine. Het afhandelen van karakters was onhandig, pointers moesten worden geschaald tussen woord- en byte-adressen, en floating-point waarden pasten niet in een woord.

Het typesysteem van C werd gecreëerd om die mismatch op te lossen. char gaf je de byte. int behield de geest van het woord uit BCPL: de natuurlijke integer van de machine. Dat idee werd onderdeel van de taal. Zelfs vandaag zegt de C-standaard (C11 §6.2.5):

"A 'plain' int object has the natural size suggested by the architecture of the execution environment."

Dit is een bewuste ontwerpkeuze. int was nooit bedoeld om "32 bits" te zijn. Het betekende: "waarmee deze machine het snelst en comfortabelst werkt".

C ontsnapt aan de PDP-11

De echte test kwam in 1977 en 1978, toen Ritchie en Steve Johnson UNIX en C poortten naar de Interdata 8/32, een 32-bit machine die sterk verschilde van de PDP-11. Ze schreven hierover in Portability of C Programs and the UNIX System (Bell System Technical Journal, 1978). Rond dezelfde tijd maakte de Portable C Compiler (pcc) van Johnson het praktisch om C naar nieuwe architecturen te richten, en C verspreidde zich naar een breed scala aan hardware.

De eerste editie van The C Programming Language (Kernighan & Ritchie, 1978) bevat een tabel die de type-groottes op vier machines laat zien waar C destijds al op draaide:

TypeDEC PDP-11Honeywell 6000IBM 370Interdata 8/32
char8 bits9 bits8 bits8 bits
short16361616
int16363232
long32363232
float32363232
double64726464

In 1978 draaide C al op machines met 16-bit ints, 32-bit ints en 36-bit ints, met 8-bit en 9-bit chars. De flexibele groottes waren er vanaf het begin en ze werkten. Dezelfde taal, en grotendeels dezelfde programma's, draaiden native en efficiënt op al deze machines.

Waarom flexibele groottes nuttig waren

Stel dat C had vereist dat int precies 32 bits was, two's complement, met wrapping bij overloop, zoals veel moderne talen doen. Dit is wat dat zou hebben gekost:

1. Op een 16-bit machine (PDP-11, 8086)

Elke int-operatie zou twee machine-instructies nodig hebben in plaats van één. Het optellen van twee 32-bit waarden op een PDP-11 betekent een ADD op de lage woorden gevolgd door een ADC (add with carry) op de hoge woorden. Vergelijkingen, shifts en vermenigvuldigingen worden allemaal slechter. Elke array-index en elke loop-teller zou twee keer zoveel registers en instructies kosten.

int i;
for (i = 0; i < n; i++) {
    total += table[i];
}

Met een "natuurlijke" int compileert deze loop naar single-register operaties op een PDP-11, een IBM 370, een Honeywell 6000 of een Cray. Met een verplichte 32-bit int is de PDP-11 versie ongeveer twee keer zo traag, en de PDP-11 was de machine waarvoor C werd geschreven.

2. Op een 36-bit machine (PDP-10, Honeywell, UNIVAC)

Een vaste 32-bit int is hier waarschijnlijk nog erger, omdat de hardware groter is dan het type. Om exact 32-bit wraparound te behouden, zou de compiler na bijna elke rekenkundige operatie moeten maskeren:

/* wat "a = a + b" zou moeten worden voor exacte 32-bit semantiek */
a = (a + b) & 0xFFFFFFFF;
/* ...plus handmatige sign-extension vanaf bit 31 als a signed is */

Dat zijn extra instructies bij elke optelling, aftrekking en vermenigvuldiging, en 4 bits die verspild worden in elk woord. int 36 bits laten zijn kost niets.

3. Op een Ones'-Complement machine (UNIVAC 1100 / Unisys 2200, CDC 6600)

Als de taal two's-complement wraparound zou vereisen, zou elke signed-operatie op een ones'-complement machine software-emulatie nodig hebben. C vermeed dit door alle drie de signed representaties (two's complement, ones' complement, sign-magnitude) toe te staan en door signed overflow als undefined behavior te markeren.

Noot: Tegenwoordig lezen mensen undefined behavior vaak als een geschenk voor optimizers. Het begon als een manier om het overflow-gedrag van de ene machine niet af te dwingen op elke andere machine.

4. Op machines zonder 8-bit bytes (CDC, PDP-10, Word-Addressed DSPs)

C vereist dat char minimaal 8 bits heeft (CHAR_BIT >= 8), niet exact 8. Op een 36-bit machine passen 9-bit chars precies vier keer in een woord. Op een machine die alleen hele woorden kan adresseren, kan char simpelweg het woord zijn.

Dit is ook waarom C nooit heeft beloofd dat alle pointers dezelfde representatie hebben. Op de Data General Nova hielden een char en een int die naar dezelfde plek wezen verschillende bitpatronen. C's regels voor pointer-conversie (en de noodzaak voor void * en expliciete casts) bestaan vanwege machines als die.

5. Op kleine 8-bit micros (Z80, 6502, 8080)

C staat niet toe dat int zo klein is als de 8-bit registers van de machine. De standaard stelt een minimumbereik vast van -32767 tot 32767, dus int moet minimaal 16 bits zijn. Dit toont de balans in het ontwerp: de grootte past zich aan de machine aan, maar er is een ondergrens zodat programma's op iets kunnen vertrouwen.

Het ontwerppatroon: minima, geen exacte groottes

Toen ANSI C in 1989 standaardiseerde, werd deze filosofie vastgelegd. Je krijgt geen exacte groottes, maar gegarandeerde minimumbereiken, ontsloten via <limits.h>:

TypeGegarandeerd minimum
char8 bits (CHAR_BIT >= 8)
short16 bits
int16 bits
long32 bits
long long (C99)64 bits

Een draagbaar C-programma vraagt: "wat is het kleinste type dat gegarandeerd mijn bereik kan bevatten?", niet "welk type is precies N bits?". Als je waarden tot 100.000 nodig hebt, gebruik je long, omdat long overal gegarandeerd deze waarden kan bevatten. Als je waarden passen in ±32767, is int prima en zal dit de snelste keuze zijn op elke machine.

De ANSI C Rationale vatte de "geest van C" samen in een paar principes, waarvan er twee hier direct op van toepassing zijn:

  1. Vertrouw de programmeur.
  2. Maak het snel, zelfs als het niet gegarandeerd draagbaar is.

C probeerde draagbaar en snel te zijn door elke implementatie de taal op de goedkoopste manier mogelijk op de hardware te laten mappen. Dat is een trade-off... geen blunder.

Een casestudy in draagbaar C: Lua

Als je wilt zien hoe deze filosofie goed wordt toegepast in een moderne repository, moet je naar de broncode van Lua kijken. Lua draait op alles, van 64-bit servers tot microcontrollers en ongebruikelijke toolchains. Lua is geschreven in wat de auteurs "Clean C" noemen, het gemeenschappelijke subset van ANSI C (C89). Lange tijd vermeed Lua volledig de afhankelijkheid van <stdint.h> en volgt het nog steeds de oudere aanpak: vraag de compiler wat hij heeft via <limits.h> en kies types op basis van gegarandeerde bereiken.

Hier zijn enkele voorbeelden uit Lua 5.4:

Detecteren of int groot genoeg is, zonder int32_t

In luaconf.h:

/*
@@ LUAI_IS32INT is true iff 'int' has (at least) 32 bits.
*/
#define LUAI_IS32INT	((UINT_MAX >> 30) >= 3)

Dit is puur C89. Het vraagt niet "is int precies 32 bits?", want op een 36-bit machine zou dat de verkeerde vraag zijn. Het vraagt "bevat unsigned int ten minste 32 bits aan bereik?". Het werkt op 16-, 32-, 36- en 64-bit ints.

VM-instructies: "Minimaal 4 bytes", niet "Precies 4 bytes"

In llimits.h:

/*
** type for virtual-machine instructions;
** must be an unsigned with (at least) 4 bytes (see details in lopcodes.h)
*/
#if LUAI_IS32INT
typedef unsigned int l_uint32;
#else
typedef unsigned long l_uint32;
#endif
typedef l_uint32 Instruction;

Het type heet l_uint32, maar kijk naar de commentaar: het hoeft alleen maar minimaal 32 bits te zijn. Op een machine met 16-bit ints valt het terug op long, waarvan de standaard garandeert dat het minimaal 32 bits is. De instructie-decoder in lopcodes.h extraheert velden met shifts en masks, dus eventuele extra hoge bits op bredere machines worden simpelweg genegeerd.

Kleine getallen en geheugentellingen

/* chars used as small naturals (so that 'char' is reserved for characters) */
typedef unsigned char lu_byte;
typedef signed char ls_byte;

Lua gebruikt unsigned char waar een moderne codebase uint8t zou schrijven. Dat is overal correct, inclusief op machines waar een byte 9 of 16 bits is. uint8t zou op die machines niet eens bestaan.

De types voor geheugenadministratie volgen hetzelfde patroon: sizet/ptrdifft wanneer int 32 bits of meer is, en unsigned long/long op 16-bit machines waar size_t mogelijk te klein is om het totale geheugengebruik te tellen.

Het kiezen van lua_Integer op basis van het platform

Het integer-type van Lua is configureerbaar in luaconf.h (LUAINTINT, LUAINTLONG, LUAINTLONGLONG), met een LUA_32BITS optie voor kleine doelen:

#if defined(LUA_32BITS)		/* { */
/*
** 32-bit integers and 'float'
*/
#if LUAI_IS32INT  /* use 'int' if big enough */
#define LUA_INT_TYPE	LUA_INT_INT
#else  /* otherwise use 'long' */
#define LUA_INT_TYPE	LUA_INT_LONG
#endif
#define LUA_FLOAT_TYPE	LUA_FLOAT_FLOAT

Standaard wordt long long gebruikt wanneer de compiler dit biedt (gecontroleerd via LLONG_MAX) en valt het terug op long voor strikte C89-compilers die dat niet doen.

Binaire serialisatie met CHAR_BIT

Het meest leerzame voorbeeld is wellicht string.pack en string.unpack in lstrlib.c. Ze lezen en schrijven binaire integers byte voor byte, en ze gaan er niet vanuit dat een byte 8 bits is:

/* number of bits in a character */
#define NB	CHAR_BIT
/* mask for one character (NB 1's) */
#define MC	((1 << NB) - 1)

Integers worden gepakt door herhaaldelijk n & MC te nemen en naar rechts te shiften met NB. Endianness wordt expliciet in de code afgehandeld in plaats van door een pointer te casten en te hopen. Dit is hoe je binaire I/O schrijft die werkt op elke conform C-implementatie.

"Geen vaste-grootte types" betekent niet "geen controle over groottes". Je drukt uit wat je nodig hebt (een bereik, een minimale breedte, een byte), controleert dit met <limits.h> en laat de implementatie het meest efficiënte type kiezen dat daaraan voldoet. Dit is de discipline die het ontwerp van C van programmeurs verwachtte.

Om eerlijk te zijn: wat de nadelen waren

Ik wil niet doen alsof flexibele groottes gratis waren. Sommige pijnpunten zijn reëel:

  • Mensen schreven hun code niet zoals Lua dat doet. Veel C-code nam aan dat int 16 bits was (in het DOS-tijdperk) of 32 bits (later), nam aan dat sizeof(int) == sizeof(void *), of sloeg pointers op in ints. De overstap naar 32-bit en later 64-bit machines brak die code. De taal maakte draagbaarheid mogelijk, maar dwong het niet af.
  • De splitsing in 64-bit datamodellen. Unix-achtige systemen kozen LP64 (long en pointers zijn 64 bits), terwijl 64-bit Windows koos voor LLP64 (long blijft 32 bits en alleen long long en pointers zijn 64 bits). Code die long gebruikte als "de grote integer" of "de pointer-grootte integer" gedraagt zich op deze twee systemen anders.
  • Standaard vaste-breedte types kwamen laat aan. <stdint.h> kwam pas met C99, waardoor elk project gedurende twee decennia zijn eigen u32/INT32 typedefs schreef met #ifdef-bossen. Dat is precies de "gebruik ifdefs"-workaround die mensen vandaag suggereren, en de standaard heeft dit uiteindelijk officieel gemaakt.

Zelfs <stdint.h> behield de oorspronkelijke filosofie van C. De exact-width types (int8t, uint32t, etc.) zijn optioneel: een implementatie biedt ze alleen aan als hij daadwerkelijk een type van die exacte breedte heeft zonder padding-bits en met een two's-complement representatie. De verplichte types zijn intleast32t ("het kleinste type met minimaal 32 bits") en intfast32t ("het snelste type met minimaal 32 bits"). Deze zijn in feite het ANSI-idee van minimumbereiken met duidelijkere namen. De commissie weigerde nog steeds te doen alsof elke machine een 8-bit byte heeft.

Noot: Dit is niet alleen geschiedenis. Veel DSP's die vandaag nog in productie zijn hebben een CHARBIT van 16 of 32 (sommige TI- en Analog Devices-onderdelen, bijvoorbeeld), en er bestaan C-compilers voor de 36-bit ones'-complement Unisys 2200-lijn. Op die systemen bestaat uint8t niet, en code geschreven zoals die van Lua compileert en draait nog steeds.

Hoe moderne talen verschillen (en waarom dat kan)

Talen die vanaf de jaren 90 zijn ontworpen, hadden een ander startpunt. De architectuuroorlogen waren voorbij. De 8-bit byte, het byte-adresseerbare geheugen en de two's-complement rekenkunde hadden effectief gewonnen. Ontwerpers konden die aannames in de taal inbouwen:

  • Java (1995) legt alles vast: byte is 8 bits, short 16, int 32, long 64, allemaal two's complement met gedefinieerde wraparound. Java kon dit mede omdat het zich richt op een virtuele machine. De JVM zorgt dat de hardware uniform oogt, en de kosten van emulatie (indien aanwezig) zijn het probleem van de JVM.
  • C# volgt hetzelfde model op de .NET runtime.
  • Rust heeft geen gewone int. Je schrijft i8, u16, i32, u64, i128, enzovoort, met expliciete breedtes. Overflow is gedefinieerd: het veroorzaakt een panic in debug-builds en wrapt in release-builds standaard, met expliciete methoden (wrappingadd, checkedadd, saturating_add) wanneer je een specifiek gedrag nodig hebt.
  • Zig gaat verder met integers van willekeurige breedte zoals u7, i3 of u48, wat nuttig is voor bitfields en hardware-registers. Het biedt ook cint, clong, etc. voor C-interop, omdat het weet dat C-types variëren.
  • Go legt int8 tot int64 vast, maar zijn gewone int en uint zijn 32 of 64 bits afhankelijk van het platform.
  • Swift doet hetzelfde: Int is woord-breed, naast expliciete Int8 tot Int64.

De laatste drie punten zijn veelzeggend. Zelfs moderne talen hielden het idee van een natuurlijke, platform-afhankelijke integer vast: Rust's isize/usize, Go's int, Swift's Int, Zig's usize. Het concept is niet verdwenen. Het is versmald van "het natuurlijke woord van de machine" naar "de grootte van een pointer", omdat geheugenindexering de plek is waar woordgrootte nog steeds van belang is.

De kernintuïtie van C bleek correct. Moderne talen hebben dit behouden en alles eromheen vastgelegd.

Zelfs C is meegegaan in de convergentie van hardware. C23 vereist eindelijk two's-complement representatie voor signed integers (C++20 deed hetzelfde). De commissie kon dit in de jaren 2020 doen omdat de ones'-complement en sign-magnitude machines die de oude regel rechtvaardigden, extreem zeldzaam waren geworden. Dat is de juiste volgorde van gebeurtenissen: de standaard volgde de realiteit zodra de realiteit was gestabiliseerd.

Draagbaarheid is niet altijd een vereiste

Houd er ook rekening mee dat sommige projecten geen extreme draagbaarheid vereisen. Wanneer we bijvoorbeeld C schrijven in onze PlayStation Programming-cursus, weten we precies wat de doelmachine is. Er is geen giswerk over groottes. De CPU is altijd de MIPS R3000, de geheugenuitlijning is altijd die welke vereist is door de MIPS RISC CPU, de compiler die we gebruiken is die van de originele Sony SDK, en we weten met absolute zekerheid hoe groot onze ints, shorts en longs zijn.

Veel van de C-code die we in onze projecten schrijven, volgt precies deze realiteit. In dergelijke gevallen schaadt het niet om simpelweg te vertrouwen op de native C-types.

Tenzij je besluit om dezelfde C-code te gebruiken bij het porten van het spel naar andere platforms. Wat precies gebeurde met de PS1-broncode van Wipeout toen Psygnosis het spel portte naar DOS en Windows PC. 😂

Conclusie

Een ontwerpbeslissing moet worden beoordeeld op het probleem dat het oploste en de wereld waarvoor het werd gemaakt. Het probleem van C in de jaren 70 was om een systeemtaal te zijn die assembly kon vervangen op machines met 12-, 16-, 18-, 24-, 32-, 36-, 48-, 60- en 64-bit woorden, met 6-, 7-, 8- en 9-bit karakters, en met drie verschillende manieren om negatieve getallen weer te geven, terwijl er code werd gegenereerd die bijna net zo goed was als handgeschreven assembly.

Vaste integer-groottes zouden C traag of onpraktisch hebben gemaakt op veel van die machines. Door de groottes open te laten, ondersteund door gegarandeerde minimumbereiken, kon C bijna overal draaien. Dat is een groot deel van de reden waarom UNIX kon worden geport, en een groot deel van de reden waarom C de taal werd waarop bijna alle latere systeemsoftware is gebouwd.

Als je zeker weet dat je alleen moderne 64-bit, 8-bit-byte, two's-complement machines target, gebruik dan een taal die voor die wereld is ontworpen, of gebruik <stdint.h> in C. Maar als je, net als ik, nog steeds code schrijft voor oude en ongebruikelijke machines, doen C's flexibele integers nog steeds het werk waarvoor ze zijn ontworpen.

Het was geen fout. Draagbaarheid was juist het hele punt!

Verder lezen

  • Dennis M. Ritchie, The Development of the C Language (HOPL-II, 1993)
  • S. C. Johnson and D. M. Ritchie, Portability of C Programs and the UNIX System (Bell System Technical Journal, 1978)
  • Kernighan and Ritchie, The C Programming Language, 1st ed. (1978), §2.2
  • Rationale for American National Standard for Information Systems, Programming Language C (ANSI X3.159-1989)
  • Lua 5.4 source: luaconf.h, llimits.h, lopcodes.h, lstrlib.c