PONYλM2Modula-2

C.CodeCompared.To/Pascal

An interactive executable cheatsheet comparing C and Pascal

C17 (GCC) Free Pascal 3.2.2
Program Structure & Output
Hello, World
The same shape, spelled in words rather than punctuation: begin and end instead of braces, a full stop rather than a closing brace at the very end, and no include — writeln is part of the language.
#include <stdio.h> int main(void) { printf("Hello, World!\n"); return 0; }
program hello; begin writeln('Hello, World!'); end.
The final end. — with a period, not a semicolon — is what ends the program text; anything after it is ignored. Single quotes delimit strings and double quotes have no meaning at all, so an apostrophe inside a literal is doubled: 'it''s'. writeln adds the newline, write does not, and both accept any number of arguments of any printable type without a format string.
The Semicolon Separates; It Does Not Terminate
This is the syntactic difference you will trip over first. In C every statement ends with a semicolon. In Pascal the semicolon goes between statements, so the last one before an end does not need it — and an extra one before else is an error.
#include <stdio.h> int main(void) { int value = 5; if (value > 3) { printf("big\n"); /* semicolon required */ } else { printf("small\n"); /* and here */ } return 0; }
program separators; var value: Integer; begin value := 5; if value > 3 then begin writeln('big') // NO semicolon: next token is end end else begin writeln('small') end; // semicolon separates this from what follows writeln('done') end.
A semicolon before else is a syntax error, because it would end the if statement and leave the else attached to nothing. In practice compilers accept a trailing semicolon before end — it simply separates the statement from an empty one — so the rule bites almost exclusively at else. Note also that then takes a single statement, which is why the begin/end pair is needed for two.
Formatting Without a Format String
writeln knows the type of every argument, so there are no conversions to get wrong. Width and precision are attached to the value with colons — value:8:2 is eight columns with two decimals — and Format from SysUtils is there when you want a real format string.
#include <stdio.h> int main(void) { const char *name = "Ada"; int count = 3; double average = 2.5; printf("%s has %d items averaging %.2f\n", name, count, average); printf("%10s|%-10s|\n", "right", "left"); printf("%8.2f\n", 1234.5); return 0; }
program formatting; uses SysUtils; var name: string; count: Integer; average: Double; begin name := 'Ada'; count := 3; average := 2.5; writeln(name, ' has ', count, ' items averaging ', average:0:2); writeln('right':10, '|', 'left', ' |'); writeln(1234.5:8:2); writeln(Format('%s has %d items averaging %.2f', [name, count, average])); end.
The colon syntax is positional: one colon is a minimum field width, two is width and decimal places. Without them a Double prints in scientific notation with far more digits than you wanted, which is the first thing everyone hits. Format takes its arguments as an array of variants, so it counts them — passing too few raises EConvertError rather than reading whatever was on the stack.
Command-Line Arguments
There is no argument list on the program header. Two functions ask the runtime instead, so any procedure can reach them rather than only the entry point.
#include <stdio.h> int main(int argc, char *argv[]) { printf("argument count: %d\n", argc - 1); printf("program name: present\n"); for (int index = 1; index < argc; index++) { printf(" %d: %s\n", index, argv[index]); } return 0; }
program arguments; var index: Integer; begin writeln('argument count: ', ParamCount); writeln('program name: present'); for index := 1 to ParamCount do writeln(' ', index, ': ', ParamStr(index)); end.
ParamCount excludes the program name, which argc includes — so the C column subtracts one to agree. ParamStr(0) is the program name, matching argv[0]. Because both are functions rather than parameters, a procedure buried five levels down can read them without anything being threaded through, which in C means a global.
:= Assigns, = Compares
:= Assigns and = Compares
C uses = for assignment and == for comparison, and an assignment is an expression — which is why if (x = 1) compiles and does the wrong thing. Pascal gives the two operations different symbols and makes assignment a statement, so the mistake has no spelling.
#include <stdio.h> int main(void) { int value = 5; /* The classic typo. It assigns, then tests the assigned value, so this branch is always taken. Most compilers warn only if asked. */ if (value = 1) { printf("value is now %d\n", value); } if (value == 1) { printf("value equals 1\n"); } return 0; }
program assignment; var value: Integer; begin value := 5; // if value := 1 then is a syntax error: an assignment is a // STATEMENT, and a statement is not a condition. if value = 1 then writeln('value equals 1') else writeln('value is ', value); end.
The C column prints value is now 1, having silently overwritten the variable it was meant to test. Requiring two different symbols costs one keystroke per assignment and removes the bug entirely — which is the trade Pascal makes over and over. The same reasoning is why there is no ++: Inc(value) is a procedure call, so it cannot appear inside an expression and cannot be evaluated twice by accident.
Declarations Come First, in Labelled Blocks
C lets a declaration appear anywhere a statement can. Pascal collects them into blocks before the body — const, type, var, in that order — so everything a procedure uses is listed in one place above the code.
#include <stdio.h> #define MAXIMUM 10 int main(void) { int count = 0; for (int index = 0; index < 3; index++) { /* declared inline */ count += index; } double average = (double)count / 3.0; /* and here */ printf("%d %.2f %d\n", count, average, MAXIMUM); return 0; }
program declarations; const Maximum = 10; type TCounter = Integer; var count: TCounter; index: Integer; average: Double; begin count := 0; for index := 0 to 2 do count := count + index; average := count / 3.0; writeln(count, ' ', average:0:2, ' ', Maximum); end.
It reads as bureaucracy until you notice what it buys: every name a block uses is visible in a dozen lines at the top, there is no shadowing surprise from a declaration halfway down, and the loop variable is a genuine local of the procedure rather than of the loop. The cost is real too — a long procedure’s declarations drift far from their use, which is an argument for shorter procedures rather than for the other rule.
A Type Alias Can Be a Real, Distinct Type
A C typedef introduces a new spelling for an existing type and nothing more, so the compiler cannot tell a Metres from a Feet. Pascal’s type section creates a genuinely new type, and assigning between two of them is an error.
#include <stdio.h> typedef int Metres; typedef int Feet; int main(void) { Metres distance = 100; Feet height = 6; /* Both are int, so this compiles and means nothing sensible. */ distance = height; printf("distance is now %d\n", distance); return 0; }
program distinct_types; type TMetres = type Integer; // "type" makes it genuinely distinct TFeet = type Integer; var distance: TMetres; height: TFeet; begin distance := 100; height := 6; // distance := height; is an error: incompatible types distance := TMetres(height); // an explicit cast still works writeln('distance is now ', distance); end.
Without the extra type keyword — plain TMetres = Integer — you get C’s behavior, an alias that is assignment-compatible with the original. With it you get a distinct type that must be cast, which is how Pascal lets you attach meaning to an integer without paying for a wrapper struct. The cast is still available, so the check is a speed bump rather than a wall; that is usually exactly the right strength.
Types the Compiler Actually Enforces
An Enumeration Is a Type, Not an Integer
A C enum constant is an int, so any integer fits in an enum variable and the compiler shrugs. A Pascal enumeration is a distinct type whose only values are the ones you named.
#include <stdio.h> enum Colour { RED, GREEN, BLUE }; int main(void) { enum Colour shade = GREEN; printf("green is %d\n", shade); /* Nothing stops this: an enum is an int. */ shade = 99; printf("shade is now %d\n", shade); /* And there is no way to ask how many there are. */ printf("last is %d\n", BLUE); return 0; }
program enumerations; type TColour = (Red, Green, Blue); var shade: TColour; begin shade := Green; writeln('green is ', Ord(shade)); // shade := 99; is an error: incompatible types writeln('first is ', Ord(Low(TColour))); writeln('last is ', Ord(High(TColour))); writeln('after green is ', Ord(Succ(shade))); for shade := Low(TColour) to High(TColour) do write(Ord(shade), ' '); writeln; end.
Low, High, Succ and Pred work on any ordinal type, so an enumeration can be looped over without anyone maintaining a COLOUR_COUNT constant that goes stale — which is the C idiom this replaces. Ord is the deliberate one-way door to the integer; there is no implicit conversion in either direction, and TColour(1) is the explicit way back.
A Subrange Puts the Valid Range in the Type
When a C value must stay between 0 and 9, the constraint lives in a comment and an assert. Pascal has a type for it, and with range checking on the compiler enforces it at every assignment.
#include <stdio.h> #include <assert.h> int main(void) { int digit = 5; /* The range is documentation plus a runtime check you wrote. */ assert(digit >= 0 && digit <= 9); printf("digit %d\n", digit); digit = digit + 10; /* Nothing complains until the next assert — if there is one. */ printf("digit is now %d\n", digit); return 0; }
program subranges; uses SysUtils; type TDigit = 0..9; var digit: TDigit; begin digit := 5; writeln('digit ', digit); writeln('range ', Low(TDigit), ' to ', High(TDigit)); {$RANGECHECKS ON} try digit := digit + 10; writeln('digit is now ', digit); except on error: ERangeError do writeln('ERangeError: the type says 0..9'); end; {$RANGECHECKS OFF} end.
Range checking is off by default and is turned on per-region with {$RANGECHECKS ON} or for the whole build with -Cr — so this is a deliberate cost you pay where correctness matters more than speed. Even with checking off the type is still useful: the compiler sizes the variable from the range, Low and High report it, and an array [TDigit] of … is exactly ten elements with no separate constant to keep in step.
set of: Bit Flags With a Type
A C flags word is an int, some #defines, and the discipline to use the right constants with the right variable. A Pascal set is a real type built from an ordinal type, with operators for union, intersection and membership.
#include <stdio.h> #define FLAG_READ (1 << 0) #define FLAG_WRITE (1 << 1) #define FLAG_EXEC (1 << 2) int main(void) { int permissions = FLAG_READ | FLAG_WRITE; printf("has write: %d\n", (permissions & FLAG_WRITE) != 0); permissions &= ~FLAG_WRITE; printf("after clear: %d\n", permissions); /* Nothing stops mixing flag families: */ permissions |= 64; printf("with a stray bit: %d\n", permissions); return 0; }
program sets; type TPermission = (Read, Write, Execute); TPermissions = set of TPermission; var permissions: TPermissions; vowels: set of Char; begin permissions := [Read, Write]; writeln('has write: ', Write in permissions); permissions := permissions - [Write]; writeln('after clear: ', Read in permissions, ' ', Write in permissions); permissions := permissions + [Execute]; writeln('now has execute: ', Execute in permissions); vowels := ['a', 'e', 'i', 'o', 'u']; writeln('e is a vowel: ', 'e' in vowels); end.
The operators are arithmetic ones with set meanings: + is union, - is difference, * is intersection, and in is membership. Because the element type is an enumeration, there is no way to put a stray bit in — the C column’s last line has no spelling here. Under the hood it is the same bitmask, one bit per possible element, which is why a set is limited to 256 elements.
A Boolean Is Not a Number
C’s conditions are integers, which is what makes if (count) work and if (pointer) idiomatic. Pascal has a distinct Boolean type with no implicit conversion in either direction.
#include <stdio.h> int main(void) { int count = 0; if (!count) { printf("count is zero\n"); } /* A comparison yields an int, so this is arithmetic: */ int flag = (count == 0); printf("flag plus one: %d\n", flag + 1); return 0; }
program booleans; var count: Integer; flag: Boolean; begin count := 0; if count = 0 then writeln('count is zero'); flag := (count = 0); writeln('flag: ', flag); // flag + 1 is a type error. Ord() is the way across: writeln('flag plus one: ', Ord(flag) + 1); end.
The operators are words — and, or, not, xor — and in objfpc mode they short-circuit for Booleans, so if (p <> nil) and (p^.value > 0) is safe. Note the brackets around each comparison: and binds tighter than =, which is the opposite of C, and forgetting them is the one precedence trap the language has.
Strings That Carry Their Length
A Short String Is a Length Byte and a Buffer
This is the representation Pascal is famous for, and it is exactly what a careful C programmer builds by hand: a fixed buffer with the length stored in front of it, rather than a terminator hidden inside the data.
#include <stdio.h> #include <string.h> /* The hand-rolled equivalent: a length and a buffer. */ struct CountedString { unsigned char length; char bytes[255]; }; int main(void) { struct CountedString text; const char *source = "hello"; text.length = (unsigned char)strlen(source); memcpy(text.bytes, source, text.length); printf("length %u\n", text.length); printf("bytes %zu\n", sizeof text); printf("value %.*s\n", (int)text.length, text.bytes); return 0; }
program short_strings; var text: string; // in objfpc mode without {$H+}: a ShortString begin text := 'hello'; writeln('length ', Length(text)); writeln('bytes ', SizeOf(text)); writeln('value ', text); // The length really is byte zero of the variable: writeln('byte 0 ', Ord(text[0])); writeln('char 1 ', text[1]); // subscripts start at 1 end.
Both columns report 256 bytes, because that is what the structure is: one length byte and 255 characters. Two consequences follow. Subscripts start at 1, since index 0 is the length — and you can read it as text[0], which is the same trick as reading text.length in the C column. And the maximum length is 255, silently: assigning a longer value truncates it.
AnsiString: The One That Grows
The 255-character limit is why AnsiString exists. It is a reference-counted pointer to a heap block carrying a length and a count, so it grows to whatever it holds and frees itself when the last reference goes away.
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { /* A string of unknown length means malloc, and a free to match. */ const char *first = "hello"; const char *second = ", world"; size_t length = strlen(first) + strlen(second); char *joined = malloc(length + 1); if (joined == NULL) return 1; strcpy(joined, first); strcat(joined, second); printf("[%s] length %zu\n", joined, strlen(joined)); printf("variable is %zu bytes\n", sizeof joined); free(joined); return 0; }
program ansi_strings; var joined: AnsiString; long: AnsiString; index: Integer; begin joined := 'hello' + ', world'; writeln('[', joined, '] length ', Length(joined)); writeln('variable is ', SizeOf(joined), ' bytes'); long := ''; for index := 1 to 300 do long := long + 'x'; writeln('grown to ', Length(long)); end.
The variable itself is eight bytes — a pointer — which is why SizeOf reports the same number the C column does for its char *. What differs is what happens next: there is no free, because the reference count drops to zero when the variable leaves scope. Adding {$H+} at the top of a unit makes a bare string mean AnsiString, and most real Free Pascal code turns it on.
Concatenate, Copy, Find, Compare
Every operation returns a value rather than filling a buffer you sized, and comparison is the operators you would expect rather than a function whose result you have to remember the sign of.
#include <stdio.h> #include <string.h> int main(void) { const char *text = "the quick brown fox"; printf("length %zu\n", strlen(text)); printf("find %ld\n", (long)(strstr(text, "quick") - text)); char piece[6]; strncpy(piece, text + 4, 5); piece[5] = '\0'; printf("substr %s\n", piece); printf("equal %d\n", strcmp("apple", "apple") == 0); printf("before %d\n", strcmp("apple", "banana") < 0); return 0; }
program string_operations; uses SysUtils; var text: string; begin text := 'the quick brown fox'; writeln('length ', Length(text)); writeln('find ', Pos('quick', text)); writeln('substr ', Copy(text, 5, 5)); writeln('equal ', 'apple' = 'apple'); writeln('before ', 'apple' < 'banana'); writeln('upper ', UpperCase(text)); writeln('replaced ', StringReplace(text, 'brown', 'red', [])); end.
Pos returns a 1-based position and 0 when the substring is absent, which is why the two columns print 4 and 5 for the same match. The comparison operators are the real win: = compares contents, so the C habit of writing if (a == b) for strings — a bug there, comparing addresses — is correct here. Copy(text, start, count) is substr, with no destination buffer to size.
PChar Is the char * You Already Know
When Pascal has to talk to C it uses PChar: a pointer to a NUL-terminated sequence of characters, with all of C’s properties and none of Pascal’s. Converting between it and a Pascal string is explicit, and the direction matters.
#include <stdio.h> #include <string.h> int main(void) { const char *text = "hello"; /* strlen has to walk to the terminator every time. */ printf("length %zu\n", strlen(text)); printf("first %c\n", text[0]); printf("bytes %zu\n", strlen(text) + 1); /* including the NUL */ return 0; }
program pchars; var text: AnsiString; raw: PChar; begin text := 'hello'; raw := PChar(text); // points at the same bytes, NUL-terminated writeln('length ', StrLen(raw)); // walks to the terminator writeln('first ', raw[0]); // PChar subscripts start at 0 writeln('bytes ', StrLen(raw) + 1); writeln('back ', AnsiString(raw)); // copies into a Pascal string end.
Note the subscript base flips: a Pascal string starts at 1, a PChar starts at 0, because it is a C pointer. PChar(text) does not copy — it hands out the address of the AnsiString’s buffer, which Free Pascal keeps NUL-terminated for exactly this purpose — so the pointer becomes invalid as soon as the string is reassigned or goes out of scope. That is the same lifetime discipline C makes you keep, arriving back at the one place the languages meet.
Bounds Are Part of the Type
The Bounds Are Part of the Type
A C array parameter is a pointer, so the length travels separately and the two can disagree. In Pascal the bounds belong to the type: an array [1..5] of Integer and an array [1..6] of Integer are different types, and a procedure declared for one will not accept the other.
#include <stdio.h> /* The length is a promise the caller makes. */ static int sum_of(const int *values, size_t count) { int total = 0; for (size_t index = 0; index < count; index++) { total += values[index]; } return total; } int main(void) { int numbers[5] = { 3, 1, 4, 1, 5 }; printf("sum %d\n", sum_of(numbers, 5)); /* And nothing stops the caller lying about it: */ printf("lied %d\n", sum_of(numbers, 3)); return 0; }
program array_bounds; type TFive = array [1..5] of Integer; var numbers: TFive = (3, 1, 4, 1, 5); function SumOf(const values: TFive): Integer; var position: Integer; begin Result := 0; for position := Low(values) to High(values) do Result := Result + values[position]; end; begin writeln('sum ', SumOf(numbers)); writeln('bounds ', Low(numbers), ' to ', High(numbers)); end.
There is no length parameter, because the type carries it and Low/High read it back — so the C column’s last line, where the caller passes the wrong count and gets a plausible wrong answer, cannot be written. The cost is rigidity: a procedure for TFive takes only that. The escape is an open array, array of Integer as a parameter type, which accepts any length and still supplies High.
A Bad Subscript Can Be Caught
Reading past the end of a C array is undefined behavior with no diagnosis. With range checking on, a Pascal subscript outside the declared bounds raises a catchable error naming the line.
#include <stdio.h> int main(void) { int numbers[3] = { 10, 20, 30 }; int wanted = 5; /* numbers[5] is undefined behavior, so the check is yours. */ if (wanted >= 0 && wanted < 3) { printf("%d\n", numbers[wanted]); } else { printf("index %d is out of range\n", wanted); } return 0; }
program range_checks; uses SysUtils; var numbers: array [1..3] of Integer = (10, 20, 30); wanted: Integer; begin wanted := 5; {$RANGECHECKS ON} try writeln(numbers[wanted]); except on error: ERangeError do writeln('index ', wanted, ' is out of range'); end; {$RANGECHECKS OFF} end.
The check costs a compare and a branch per subscript and is off by default, which is the honest engineering position: you turn it on with -Cr for development and testing and decide per project whether to ship with it. A constant subscript outside the bounds is rejected at compile time regardless of the switch, which is the free half of the deal.
Dynamic Arrays Without malloc
An array of T with no bounds is a dynamic array: SetLength sizes it, Length reports it, and it is reference-counted so it frees itself. It is also 0-based, unlike everything else in the language.
#include <stdio.h> #include <stdlib.h> int main(void) { int count = 5; int *numbers = malloc((size_t)count * sizeof *numbers); if (numbers == NULL) return 1; for (int index = 0; index < count; index++) { numbers[index] = index * index; } for (int index = 0; index < count; index++) { printf("%d ", numbers[index]); } printf("\n"); /* Growing means realloc, and the old pointer is now invalid. */ int *grown = realloc(numbers, 8 * sizeof *grown); if (grown == NULL) { free(numbers); return 1; } numbers = grown; printf("grown to 8\n"); free(numbers); return 0; }
program dynamic_arrays; var numbers: array of Integer; index: Integer; begin SetLength(numbers, 5); for index := 0 to High(numbers) do // dynamic arrays are 0-based numbers[index] := index * index; for index := 0 to High(numbers) do write(numbers[index], ' '); writeln; SetLength(numbers, 8); // grows, keeping the contents writeln('grown to ', Length(numbers)); // No free: the array is released when it goes out of scope. end.
The 0-based indexing is a genuine inconsistency in the language — every static array you declare starts wherever you say, usually 1, and every dynamic one starts at 0 — which is why the loops here use High rather than a literal. Assigning one dynamic array to another shares the reference rather than copying; Copy(source) is how you get an independent one, which is the same distinction as memcpy against copying a pointer.
An Open Array Parameter Takes Any Length
This is the escape from bounds-in-the-type, and it is the closest thing to C’s pointer-plus-count — except that the count travels automatically and cannot be wrong.
#include <stdio.h> static int sum_of(const int *values, size_t count) { int total = 0; for (size_t index = 0; index < count; index++) { total += values[index]; } return total; } int main(void) { int three[3] = { 1, 2, 3 }; int five[5] = { 1, 2, 3, 4, 5 }; printf("%d %d\n", sum_of(three, 3), sum_of(five, 5)); return 0; }
program open_arrays; var three: array [1..3] of Integer = (1, 2, 3); five: array [1..5] of Integer = (1, 2, 3, 4, 5); function SumOf(const values: array of Integer): Integer; var index: Integer; begin Result := 0; for index := Low(values) to High(values) do Result := Result + values[index]; end; begin writeln(SumOf(three), ' ', SumOf(five)); writeln(SumOf([10, 20, 30])); // a literal array, built at the call end.
Inside the procedure an open array is always indexed from 0 regardless of how the caller’s array was declared, which is why the loop must use Low and High rather than assume. The [10, 20, 30] at the last call is an open array constructor, built on the spot — the same idea as a C compound literal, with the length going along for the ride.
record Instead of struct
record Instead of struct
The same construct with different keywords. Fields are named, assignment copies the whole thing, and the dot has the same meaning — including through a pointer, where Pascal writes ^. instead of ->.
#include <stdio.h> struct Point { double x; double y; }; int main(void) { struct Point corner = { 3.0, 4.0 }; struct Point copy = corner; /* a real copy */ copy.x = 99.0; printf("corner %.1f %.1f\n", corner.x, corner.y); printf("copy %.1f %.1f\n", copy.x, copy.y); printf("size %zu\n", sizeof(struct Point)); return 0; }
program records; type TPoint = record x: Double; y: Double; end; var corner, copy: TPoint; begin corner.x := 3.0; corner.y := 4.0; copy := corner; // a real copy copy.x := 99.0; writeln('corner ', corner.x:0:1, ' ', corner.y:0:1); writeln('copy ', copy.x:0:1, ' ', copy.y:0:1); writeln('size ', SizeOf(TPoint)); end.
Both columns report 16 bytes, because both are two doubles laid out in order. A record can be compared field by field only if you write the comparison — there is no = for records, exactly as there is none for C structs — but unlike C you can assign a whole record to another in one statement without memcpy, and a function can return one by value.
with: The Repeated Prefix, Removed
Reaching several fields of the same record means repeating its name in C. with opens the record’s fields into scope for a block — convenient, and the one feature in Pascal that regularly causes confusion.
#include <stdio.h> struct Rectangle { double width; double height; double depth; }; int main(void) { struct Rectangle box; box.width = 3.0; box.height = 4.0; box.depth = 5.0; printf("volume %.1f\n", box.width * box.height * box.depth); return 0; }
program with_statement; type TRectangle = record width, height, depth: Double; end; var box: TRectangle; begin with box do begin width := 3.0; height := 4.0; depth := 5.0; writeln('volume ', (width * height * depth):0:1); end; end.
The catch is that a name inside the block resolves to the record’s field if it has one, shadowing any local or global of the same name — silently. Two nested withs over records that share a field name are worse still, since the inner one wins and nothing says so. Modern style uses it sparingly, or not at all; it is here because you will meet it in existing code and need to know that width is not a variable.
A Variant Record Is a Tagged Union
C’s union overlays members and leaves the tag to you. A Pascal variant record has the tag built into the declaration — it is a field like any other, and the variants are grouped under the values it can take.
#include <stdio.h> enum ValueKind { KIND_INTEGER, KIND_REAL }; struct Value { enum ValueKind kind; /* the tag is a field you remember to set */ union { int as_integer; double as_real; } data; }; int main(void) { struct Value first = { KIND_INTEGER, { .as_integer = 42 } }; struct Value second; second.kind = KIND_REAL; second.data.as_real = 2.5; printf("first %d\n", first.data.as_integer); printf("second %.1f\n", second.data.as_real); printf("size %zu\n", sizeof(struct Value)); /* Nothing stops reading the wrong member: */ printf("misread %d\n", second.data.as_integer != 0); return 0; }
program variant_records; type TValueKind = (KindInteger, KindReal); TValue = record case kind: TValueKind of // the tag IS the field KindInteger: (asInteger: Integer); KindReal: (asReal: Double); end; var first, second: TValue; begin first.kind := KindInteger; first.asInteger := 42; second.kind := KindReal; second.asReal := 2.5; writeln('first ', first.asInteger); writeln('second ', second.asReal:0:1); writeln('size ', SizeOf(TValue)); // Free Pascal permits reading the inactive variant, same as C: writeln('misread ', Ord(second.asInteger <> 0)); end.
The case in a type declaration is a different construct from the statement, and the fields it introduces sit at the same address — so this is genuinely a union, with the tag stored alongside. The standard says reading a variant other than the active one is undefined; Free Pascal permits it, which means it is also the idiomatic way to reinterpret bits, the same use C unions get put to.
packed record: Layout Without Padding
A C compiler inserts padding so each field is aligned, and #pragma pack is the non-standard way to stop it. Pascal has the keyword in the language: a packed record has no padding at all.
#include <stdio.h> #include <stddef.h> struct Header { unsigned char kind; int length; unsigned char flags; }; #pragma pack(push, 1) struct PackedHeader { unsigned char kind; int length; unsigned char flags; }; #pragma pack(pop) int main(void) { printf("padded %zu\n", sizeof(struct Header)); printf("packed %zu\n", sizeof(struct PackedHeader)); printf("length at %zu\n", offsetof(struct Header, length)); return 0; }
program packed_records; type THeader = record kind: Byte; length: LongInt; flags: Byte; end; TPackedHeader = packed record kind: Byte; length: LongInt; flags: Byte; end; var padded: THeader; begin writeln('padded ', SizeOf(THeader)); writeln('packed ', SizeOf(TPackedHeader)); writeln('length at ', PtrUInt(@padded.length) - PtrUInt(@padded)); end.
Both columns report 12 padded and 6 packed. A packed record is what you declare for anything read from a file or a socket, because the layout then depends on nothing but the field list — and the cost is the same one C pays, an unaligned field access that is slower on every architecture and a fault on some. The names differ slightly and are worth learning: Byte, Word, LongInt and Int64 are the fixed 8, 16, 32 and 64-bit types, where Integer’s width depends on the mode.
Typed Pointers, No Arithmetic
Pointers Are Typed, and There Is No Arithmetic
A Pascal pointer names a type: ^Integer points at an integer and at nothing else. Dereferencing is a postfix ^ rather than a prefix *, and — the important part — you cannot add to it.
#include <stdio.h> int main(void) { int value = 42; int *pointerToValue = &value; printf("through the pointer %d\n", *pointerToValue); *pointerToValue = 99; printf("value is now %d\n", value); printf("null check: %d\n", pointerToValue == NULL); /* And the part Pascal has no spelling for: */ int numbers[5] = { 1, 2, 3, 4, 5 }; int *walk = numbers; walk += 2; printf("arithmetic reaches %d\n", *walk); return 0; }
program typed_pointers; type PInteger = ^Integer; var value: Integer; pointerToValue: PInteger; begin value := 42; pointerToValue := @value; // @ takes the address writeln('through the pointer ', pointerToValue^); pointerToValue^ := 99; // assign through it writeln('value is now ', value); // pointerToValue := pointerToValue + 1; is an error: no arithmetic writeln('nil check: ', pointerToValue = nil); end.
The ^ is postfix, which reads better in a chain: C’s node->next->value is node^.next^.value. Removing arithmetic removes the ability to walk off the end of anything by mistake — the price is that iterating over a buffer means subscripting an array rather than incrementing a pointer, which is what you should have been doing anyway. Free Pascal will re-enable arithmetic on typed pointers with {$POINTERMATH ON}, which is the deliberate way back to the C model.
New and Dispose Instead of malloc and free
The pair is the same idea with the size removed: New knows how big the pointed-to type is, so there is no sizeof to get wrong and no cast on the result.
#include <stdio.h> #include <stdlib.h> struct Node { int value; struct Node *next; }; int main(void) { struct Node *first = malloc(sizeof *first); if (first == NULL) return 1; first->value = 1; first->next = malloc(sizeof *first->next); if (first->next == NULL) { free(first); return 1; } first->next->value = 2; first->next->next = NULL; for (struct Node *walk = first; walk != NULL; walk = walk->next) { printf("%d ", walk->value); } printf("\n"); free(first->next); free(first); return 0; }
program new_and_dispose; type PNode = ^TNode; TNode = record value: Integer; next: PNode; end; var first, second, walk: PNode; begin New(first); first^.value := 1; New(second); second^.value := 2; second^.next := nil; first^.next := second; walk := first; while walk <> nil do begin write(walk^.value, ' '); walk := walk^.next; end; writeln; Dispose(second); Dispose(first); end.
Note the forward reference: PNode is declared as ^TNode before TNode exists, which is legal only inside a type block and is the one place Pascal permits using a name before defining it. New and Dispose carry every obligation malloc and free do — this is the one corner of the language where the leak, the double free and the dangling pointer are all still available. GetMem and FreeMem are the untyped, byte-counted pair when you want them.
Pointer Is void *, and Casting Is Explicit
The generic pointer type is spelled Pointer. It converts to and from any typed pointer, which makes it the same escape hatch void * is — and the same place the type system stops helping.
#include <stdio.h> #include <stdlib.h> #include <string.h> int main(void) { /* A raw block, then a cast to say what it holds. */ void *block = malloc(4 * sizeof(int)); if (block == NULL) return 1; int *numbers = (int *)block; for (int index = 0; index < 4; index++) { numbers[index] = index * 10; } printf("%d %d %d %d\n", numbers[0], numbers[1], numbers[2], numbers[3]); memset(block, 0, 4 * sizeof(int)); printf("after clear %d\n", numbers[0]); free(block); return 0; }
program untyped_pointers; type TFour = array [0..3] of Integer; PFour = ^TFour; var block: Pointer; numbers: PFour; index: Integer; begin GetMem(block, SizeOf(TFour)); numbers := PFour(block); // the cast that says what it holds for index := 0 to 3 do numbers^[index] := index * 10; writeln(numbers^[0], ' ', numbers^[1], ' ', numbers^[2], ' ', numbers^[3]); FillChar(block^, SizeOf(TFour), 0); writeln('after clear ', numbers^[0]); FreeMem(block, SizeOf(TFour)); end.
FillChar is memset and Move is memmove — note the argument order of Move(source, destination, count), which is the opposite of memcpy’s and is a reliable source of mistakes when switching between the two. FreeMem can be given the size or not; supplying it lets the runtime check, which is the sort of optional rigour the language is built around.
Procedures, Functions, and Nesting
Two Kinds of Routine
C has one, which may return void. Pascal distinguishes a function, which yields a value and is used in an expression, from a procedure, which does something and is a statement on its own.
#include <stdio.h> static double area(double width, double height) { return width * height; } static void report(double value) { printf("value %.1f\n", value); } int main(void) { report(area(3.0, 4.0)); /* A result can be discarded with no warning: */ area(1.0, 2.0); return 0; }
program routines; function Area(width, height: Double): Double; begin Result := width * height; // objfpc mode gives you Result end; procedure Report(value: Double); begin writeln('value ', value:0:1); end; begin Report(Area(3.0, 4.0)); // Area(1.0, 2.0); is an error: a function call is an expression, // and an expression is not a statement. end.
Result is a Free Pascal and Delphi addition that {$mode objfpc} turns on; standard Pascal assigns to the function’s own name instead, which also works here. The split means the C habit of calling something purely for its side effect and dropping the result has no spelling, so a routine that does work is declared as the procedure it is — and a stray call to a function is caught rather than ignored.
var, out and const Instead of Pointers
Parameters are by value by default, as in C. The three modifiers say what a routine intends to do with each one: var for read and write, out for write-only, and const for read-only — and const also permits passing a large record without copying it.
#include <stdio.h> static void swap(int *left, int *right) { int held = *left; *left = *right; *right = held; } static int divide(int numerator, int denominator, int *quotient) { if (denominator == 0) return 0; *quotient = numerator / denominator; return 1; } int main(void) { int first = 1, second = 2; swap(&first, &second); printf("%d %d\n", first, second); int quotient = 0; if (divide(7, 2, &quotient)) { printf("quotient %d\n", quotient); } return 0; }
program parameter_modes; procedure Swap(var left, right: Integer); var held: Integer; begin held := left; left := right; right := held; end; function Divide(numerator, denominator: Integer; out quotient: Integer): Boolean; begin if denominator = 0 then Exit(False); quotient := numerator div denominator; Result := True; end; var first, second, quotient: Integer; begin first := 1; second := 2; Swap(first, second); // no & at the call site writeln(first, ' ', second); if Divide(7, 2, quotient) then writeln('quotient ', quotient); end.
There is no & at the call, which is convenient and means you cannot tell from a call whether an argument will be modified — the declaration is where that lives. out is the mode with no C counterpart: the incoming value is discarded and the compiler warns if a path leaves it unset. Note div rather than /: integer division has its own operator here, because / always produces a Double.
A Procedure Inside a Procedure
Standard C has no nested functions — a helper used by exactly one routine still has to sit at file scope, and anything it needs must be passed to it. A nested Pascal procedure sees its parent’s locals directly.
#include <stdio.h> /* The helper cannot see main's locals, so the running total has to be threaded through as a parameter. */ static void add_to(int *total, int value) { *total += value; } int main(void) { int total = 0; for (int index = 1; index <= 4; index++) { add_to(&total, index * index); } printf("total %d\n", total); return 0; }
program nested; var total: Integer; index: Integer; procedure AddTo(value: Integer); // nested: sees total directly begin total := total + value; end; begin total := 0; for index := 1 to 4 do AddTo(index * index); writeln('total ', total); end.
The helper is scoped to its parent, so it cannot be called from anywhere else and does not need a name that avoids collisions — which is what static in a C file achieves at file granularity and this achieves at procedure granularity. Implementing it costs a hidden link to the parent’s frame, the same mechanism GCC uses for its non-standard nested-function extension; the difference is that here it is in the language.
Overloading and Default Parameters
C has one namespace for functions, which is why the standard library is full of abs/labs/fabs. Object Pascal picks the routine from the argument types, and a parameter can carry a default so one routine covers what would be three.
#include <stdio.h> #include <stdlib.h> #include <math.h> /* The type is in the name because it cannot be in the signature. */ static void describe_int(int value) { printf("an integer: %d\n", value); } static void describe_text(const char *value) { printf("a string: %s\n", value); } int main(void) { printf("%d %.1f\n", abs(-5), fabs(-5.5)); describe_int(42); describe_text("text"); return 0; }
program overloading; procedure Describe(value: Integer); overload; begin writeln('an integer: ', value); end; procedure Describe(value: string); overload; begin writeln('a string: ', value); end; procedure LogMessage(text: string; level: string = 'info'); begin writeln('[', level, '] ', text); end; begin writeln(Abs(-5), ' ', Abs(-5.5):0:1); // one name, both types Describe(42); Describe('text'); LogMessage('started'); LogMessage('careful', 'warn'); end.
Abs, Sqr and the rest are intrinsics the compiler resolves by operand type, so the abs/labs/fabs family collapses to one name without anyone writing an overload. The overload directive is required on each routine that participates — an omission means the second declaration simply hides the first, which is worth knowing because the resulting error message points somewhere else entirely.
case Does Not Fall Through
case Runs One Branch and Stops
C’s switch falls into the next label unless you write break, which is a well-known source of bugs. Pascal’s case runs exactly one branch, and its labels can be lists and ranges.
#include <stdio.h> static const char *describe(int code) { switch (code) { case 1: case 2: return "low"; case 3: case 4: case 5: return "medium"; default: return "high"; } } int main(void) { printf("%s %s %s\n", describe(1), describe(4), describe(9)); /* The classic bug: no break. */ int total = 0; switch (2) { case 2: total += 1; case 3: total += 10; default: total += 100; } printf("fell through to %d\n", total); return 0; }
program cases; function Describe(code: Integer): string; begin case code of 1, 2: Result := 'low'; 3..5: Result := 'medium'; // a range else Result := 'high'; end; end; begin writeln(Describe(1), ' ', Describe(4), ' ', Describe(9)); writeln('no fallthrough is possible'); end.
The range form 3..5 and the list form 1, 2 are what C spells as a run of empty labels. The selector must be an ordinal — an integer, a character, an enumeration or a subrange — so a case on a string is not available, and a case on an enumeration that misses a value is something the compiler will warn about, which is -Wswitch built in.
The for Loop Counts, and the Bounds Are Fixed on Entry
A C for is three arbitrary expressions and re-evaluates its condition every pass. A Pascal for counts from one ordinal value to another by one, evaluates both bounds once, and forbids assigning to the loop variable inside the body.
#include <stdio.h> int main(void) { int limit = 3; for (int index = 1; index <= limit; index++) { printf("%d ", index); limit = 5; /* the loop notices */ } printf("\n"); for (int index = 10; index >= 8; index--) { printf("%d ", index); } printf("\n"); for (int index = 10; index >= 1; index -= 3) { printf("%d ", index); } printf("\n"); return 0; }
program for_loops; var index, limit, value: Integer; begin limit := 3; for index := 1 to limit do begin write(index, ' '); limit := 5; // the loop does NOT notice end; writeln; for index := 10 downto 8 do // downto steps by -1; no other step exists write(index, ' '); writeln; value := 10; while value >= 1 do // anything else is a while loop begin write(value, ' '); value := value - 3; end; writeln; end.
The two columns print different first lines on purpose: C runs five times because the bound is re-read, Pascal three times because both bounds were evaluated before the first pass. The step is always one, up or down — a stride of three is a while loop — and the loop variable’s value after the loop is officially undefined, so reading it afterwards is a mistake the compiler will warn about.
repeat/until Instead of do/while
The same construct with the condition inverted: C loops while the condition holds, Pascal loops until it holds. The body runs at least once either way, and repeat needs no begin/end because the keywords already bracket it.
#include <stdio.h> int main(void) { int value = 1; do { printf("%d ", value); value *= 2; } while (value < 20); printf("\n"); return 0; }
program repeat_loops; var value: Integer; begin value := 1; repeat write(value, ' '); value := value * 2; until value >= 20; // UNTIL, so the condition is inverted writeln; end.
Inverting the condition is the whole difference and it is a reliable source of off-by-one thinking when moving code between the two. Break and Continue exist in Free Pascal and work as you expect; standard Pascal had neither, which is why old code is full of boolean guard variables. There is a goto, requiring the label to be declared in a label section first — deliberate friction.
Units Instead of Headers
A Unit Instead of a Header and a Source File
There is no textual inclusion. A unit has an interface section listing what it exports and an implementation section holding the code, both in one file — so a declaration and its definition cannot drift apart, and there is no include guard because nothing is pasted.
/* geometry.h */ #ifndef GEOMETRY_H #define GEOMETRY_H double circle_area(double radius); #endif /* geometry.c */ #include "geometry.h" #define PI 3.14159265358979323846 double circle_area(double radius) { return PI * radius * radius; } /* main.c */ #include <stdio.h> #include "geometry.h" int main(void) { printf("%.4f\n", circle_area(1.0)); return 0; }
unit Geometry; interface function CircleArea(radius: Double): Double; implementation const Pi = 3.14159265358979323846; function CircleArea(radius: Double): Double; begin Result := Pi * radius * radius; end; end. { main.pas } program Main; uses Geometry; begin writeln(CircleArea(1.0):0:4); end.
Everything in interface is exported and everything in implementation is private, which is the opposite default from C and the same effect as marking every internal function static. If the two disagree about a signature the unit does not compile, so there is no equivalent of a stale header. The compiler also writes a compiled interface file, so uses reads a summary rather than re-parsing the source — which is why Pascal builds have always been fast.
A Unit Can Run Code When It Loads
C has no standard way to run code before main — you call an init function and remember to. A unit may end with initialization and finalization sections that the runtime runs in dependency order, before and after the program body.
#include <stdio.h> /* The convention: an init function somebody must call, and a flag so calling it twice is harmless. */ static int ready = 0; static void library_init(void) { if (ready) return; printf("library ready\n"); ready = 1; } static void library_shutdown(void) { if (!ready) return; printf("library shut down\n"); ready = 0; } int main(void) { library_init(); printf("working\n"); library_shutdown(); return 0; }
unit Library; interface procedure DoWork; implementation procedure DoWork; begin writeln('working'); end; initialization writeln('library ready'); finalization writeln('library shut down'); end. { main.pas } program Main; uses Library; begin DoWork; end.
The runtime runs every initialization section in dependency order before the program body and every finalization in reverse afterwards, including when the program exits through an unhandled exception. That is a guarantee C’s atexit gives only for the second half, and only for the functions you remembered to register. GCC’s __attribute__((constructor)) is the closest equivalent, and it is an extension rather than part of the language.
Exceptions and try/finally
An Exception Cannot Be Ignored
A returned status can be ignored and usually nothing warns. A raised exception unwinds until something catches it, so the failure path runs whether the caller wrote one or not.
#include <stdio.h> #include <stdlib.h> int main(void) { const char *input = "not a number"; char *end = NULL; long parsed = strtol(input, &end, 10); if (end == input || *end != '\0') { printf("parse failed, and only the check found out\n"); } else { printf("parsed %ld\n", parsed); } strtol("also bad", NULL, 10); /* result ignored, silently */ printf("ignored, program continues\n"); return 0; }
program exceptions; uses SysUtils; var parsed: Integer; begin parsed := StrToInt('42'); writeln('parsed ', parsed); try parsed := StrToInt('not a number'); writeln('parsed ', parsed); except on error: EConvertError do writeln('EConvertError: ', error.Message); end; // The non-throwing sibling, for when failure is expected: writeln('default when bad: ', StrToIntDef('also bad', -1)); end.
The library offers both styles deliberately: StrToInt raises because a malformed number is usually a bug, and StrToIntDef returns a fallback because parsing user input is not. on error: EConvertError do selects by class, and a bare except catches everything — worth avoiding, since it swallows the out-of-memory and access-violation cases along with your parse error.
try/finally Instead of goto cleanup
The goto cleanup ladder handles the failures this routine noticed. try/finally also handles the exception raised three frames down, which the ladder cannot reach.
#include <stdio.h> #include <stdlib.h> int main(void) { char *first = NULL, *second = NULL; int result = 1; first = malloc(16); if (first == NULL) goto cleanup; second = malloc(16); if (second == NULL) goto cleanup; printf("both acquired\n"); result = 0; cleanup: free(second); free(first); printf("cleanup ran, result %d\n", result); return 0; }
program try_finally; uses SysUtils; var first, second: Pointer; begin GetMem(first, 16); try GetMem(second, 16); try writeln('both acquired'); raise Exception.Create('something failed deeper down'); finally FreeMem(second, 16); writeln('released second'); end; except on error: Exception do writeln('caught: ', error.Message); end; FreeMem(first, 16); writeln('released first'); end.
Pascal keeps try/finally and try/except as separate constructs — one block cannot have both, so nesting them as above is the normal shape. That is a little more typing than the combined form other languages offer, and it makes the distinction explicit: finally is about releasing, except is about deciding, and mixing the two responsibilities in one block is usually what goes wrong.
The Runtime Errors the Compiler Can Turn On
C has undefined behavior where a check would cost something. Pascal has a set of compiler switches that turn those same situations into diagnosed runtime errors, so you can pay for the check exactly where you want it.
#include <stdio.h> #include <limits.h> int main(void) { /* Each of these is undefined behavior, with no switch to make the compiler check it at run time. */ int denominator = 0; if (denominator == 0) { printf("division by zero must be checked by hand\n"); } int biggest = INT_MAX; unsigned int wrapped = (unsigned int)biggest + 1u; printf("overflow laundered through unsigned: %u\n", wrapped); return 0; }
{$OVERFLOWCHECKS ON} program runtime_errors; uses SysUtils; var denominator, result: Integer; wide: Int64; begin denominator := 0; try result := 10 div denominator; writeln(result); except on error: EDivByZero do writeln('EDivByZero, not undefined behavior'); end; try wide := High(Int64); wide := wide + 1; writeln('no overflow error: ', wide); except on error: EIntOverflow do writeln('EIntOverflow: the addition did not fit'); end; end.
Division by zero always raises; overflow and range checking are switches, off by default and turned on with {$OVERFLOWCHECKS ON}/{$RANGECHECKS ON} or the -Co/-Cr flags. Two details this example had to work around: the overflow switch has to be set before the program header rather than around the statement, and it is demonstrated on an Int64 because on a 64-bit target Free Pascal evaluates 32-bit arithmetic in 64-bit registers and truncates on the store, so an Integer overflow never trips the check at all. The design is the honest middle: the language specifies what happens rather than leaving it undefined, and lets you decide where the check is worth its cost. The usual practice is on for development and test builds, and a considered decision for release.
Classes, When You Want Them
A class Instead of a Struct and Its Functions
The C pattern is a struct and a family of functions taking a pointer to it. Object Pascal folds the first argument into the syntax and calls it Self — and unlike a record, a class instance always lives on the heap and the variable holds a reference.
#include <stdio.h> #include <stdlib.h> struct Rectangle { double width; double height; }; static struct Rectangle *rectangle_create(double width, double height) { struct Rectangle *self = malloc(sizeof *self); if (self == NULL) return NULL; self->width = width; self->height = height; return self; } static double rectangle_area(const struct Rectangle *self) { return self->width * self->height; } static void rectangle_scale(struct Rectangle *self, double factor) { self->width *= factor; self->height *= factor; } int main(void) { struct Rectangle *shape = rectangle_create(3.0, 4.0); if (shape == NULL) return 1; printf("area %.1f\n", rectangle_area(shape)); rectangle_scale(shape, 2.0); printf("scaled area %.1f\n", rectangle_area(shape)); free(shape); return 0; }
program classes; type TRectangle = class private FWidth, FHeight: Double; public constructor Create(width, height: Double); function Area: Double; procedure Scale(factor: Double); end; constructor TRectangle.Create(width, height: Double); begin FWidth := width; FHeight := height; end; function TRectangle.Area: Double; begin Result := FWidth * FHeight; end; procedure TRectangle.Scale(factor: Double); begin FWidth := FWidth * factor; FHeight := FHeight * factor; end; var shape: TRectangle; begin shape := TRectangle.Create(3.0, 4.0); try writeln('area ', shape.Area:0:1); shape.Scale(2.0); writeln('scaled area ', shape.Area:0:1); finally shape.Free; // the free you are still writing end; end.
The try/finally around the lifetime is the standard Object Pascal idiom, and it exists because there is no garbage collector: Create allocates and Free releases, with the same one-to-one obligation malloc and free have. The reward for it is a real object model — virtual, override, abstract, interfaces and properties are all here, and a method call goes through a vtable the compiler builds rather than one you fill in by hand.
virtual Instead of a Hand-Rolled vtable
Polymorphism in C is a struct of function pointers each implementation fills in, plus the discipline of pairing the right table with the right data. virtual and override build and populate that table for you, and check that the signatures match.
#include <stdio.h> struct ShapeOperations { double (*area)(const void *self); }; struct Square { double side; }; struct Circle { double radius; }; static double square_area(const void *self) { double side = ((const struct Square *)self)->side; return side * side; } static double circle_area(const void *self) { double radius = ((const struct Circle *)self)->radius; return 3.14159265358979 * radius * radius; } int main(void) { struct ShapeOperations squareOps = { square_area }; struct ShapeOperations circleOps = { circle_area }; struct Square square = { 3.0 }; struct Circle circle = { 1.0 }; printf("%.2f\n", squareOps.area(&square)); printf("%.2f\n", circleOps.area(&circle)); return 0; }
program polymorphism; type TShape = class public function Area: Double; virtual; abstract; end; TSquare = class(TShape) private FSide: Double; public constructor Create(side: Double); function Area: Double; override; end; TCircle = class(TShape) private FRadius: Double; public constructor Create(radius: Double); function Area: Double; override; end; constructor TSquare.Create(side: Double); begin FSide := side; end; function TSquare.Area: Double; begin Result := FSide * FSide; end; constructor TCircle.Create(radius: Double); begin FRadius := radius; end; function TCircle.Area: Double; begin Result := Pi * FRadius * FRadius; end; var shapes: array [0..1] of TShape; index: Integer; begin shapes[0] := TSquare.Create(3.0); shapes[1] := TCircle.Create(1.0); for index := 0 to High(shapes) do writeln(shapes[index].Area:0:2); for index := 0 to High(shapes) do shapes[index].Free; end.
The abstract on TShape.Area means there is no body and every descendant must supply one, which is the check nothing in the C column makes — a null function pointer compiles and crashes later. Both shapes fitting in one array is the other half: the C version cannot manage that without a common header struct or a void * and a tag. override is checked too, so a signature that drifts from the parent’s is an error rather than a silently separate method.
Where They Meet: external and cdecl
Calling a C Function With external and cdecl
This is the destination the page points at. You declare the C function’s signature, mark it cdecl so the arguments are pushed the way C expects, and say external with the symbol name — and the linker does the rest.
#include <stdio.h> #include <string.h> #include <stdlib.h> int main(void) { /* The functions on the other side are these, from the C library both languages already link against. */ printf("strlen %zu\n", strlen("hello")); printf("abs %d\n", abs(-5)); return 0; }
{$linklib c} program calling_c; function CStrLen(text: PChar): SizeInt; cdecl; external name 'strlen'; function CAbs(value: LongInt): LongInt; cdecl; external name 'abs'; begin writeln('strlen ', CStrLen('hello')); writeln('abs ', CAbs(-5)); end.
Three things must agree and only the calling convention is checked: the symbol name, the parameter widths, and the presence of the library at link time. {$linklib c} is what tells the linker to pull in the C library; without it the symbols are undeclared at link time on Linux, though a macOS build happens to link libc anyway — which is exactly the kind of difference that makes a local pass a poor proof. cdecl matters because Pascal’s own convention differs — arguments in the other order, and the callee cleaning the stack — so omitting it corrupts the stack rather than raising an error. A string literal converts to PChar automatically here because it is already NUL-terminated in the binary; an AnsiString variable needs the explicit PChar(…) cast from the strings section.
Naming the Library, and the Types to Use
Naming a shared library after external makes the runtime load it and find the symbol. The other half of the job is the types: the ctypes unit names one for each C type, so cint is whatever int is on this platform rather than whatever Integer happens to be.
#include <stdio.h> #include <math.h> int main(void) { printf("sqrt %.4f\n", sqrt(2.0)); printf("pow %.1f\n", pow(2.0, 10.0)); printf("floor %.0f\n", floor(-2.5)); return 0; }
{$linklib c} {$linklib m} program external_library; uses ctypes; function CSqrt(value: cdouble): cdouble; cdecl; external name 'sqrt'; function CPow(base, exponent: cdouble): cdouble; cdecl; external name 'pow'; function CFloor(value: cdouble): cdouble; cdecl; external name 'floor'; begin writeln('sqrt ', CSqrt(2.0):0:4); writeln('pow ', CPow(2.0, 10.0):0:1); writeln('floor ', CFloor(-2.5):0:0); writeln('cint is ', SizeOf(cint), ' bytes'); writeln('clong is ', SizeOf(clong), ' bytes'); end.
Writing external 'libfoo' instead of a bare external makes the runtime dynamically load that library, which is what you do for anything outside the C library itself. The ctypes unit is the discipline that keeps interop portable: Integer is 32 bits in objfpc mode but a C long is 64 on Unix and 32 on Windows, and clong is the name that tracks that. The same unit supplies csize_t, cuint8 and the rest.
Handing a Procedure to C as a Callback
A C library that takes a function pointer — qsort is the canonical one — can be given a Pascal routine, provided it is declared cdecl so the arguments arrive where C expects them.
#include <stdio.h> #include <stdlib.h> static int compare_integers(const void *left, const void *right) { int first = *(const int *)left; int second = *(const int *)right; return (first > second) - (first < second); } int main(void) { int numbers[5] = { 10, 9, 1, 100, 20 }; qsort(numbers, 5, sizeof numbers[0], compare_integers); for (int index = 0; index < 5; index++) { printf("%d ", numbers[index]); } printf("\n"); return 0; }
{$linklib c} program callbacks; uses ctypes; type TComparator = function (left, right: Pointer): cint; cdecl; procedure CQSort(base: Pointer; count, size: csize_t; compare: TComparator); cdecl; external name 'qsort'; function CompareIntegers(left, right: Pointer): cint; cdecl; var first, second: LongInt; begin first := PLongInt(left)^; second := PLongInt(right)^; if first > second then Result := 1 else if first < second then Result := -1 else Result := 0; end; var numbers: array [0..4] of LongInt = (10, 9, 1, 100, 20); index: Integer; begin CQSort(@numbers[0], Length(numbers), SizeOf(LongInt), @CompareIntegers); for index := 0 to High(numbers) do write(numbers[index], ' '); writeln; end.
Both columns print the same sorted line, with the C library doing the sorting in each. The cdecl on CompareIntegers is not optional — without it the two integers arrive in the wrong registers and the comparison returns nonsense, which sorts the array into a plausible wrong order rather than crashing. @CompareIntegers takes the routine’s address, and the array is passed as @numbers[0] because Pascal has no array-to-pointer decay to do it implicitly.
Making the Two Agree on a Record’s Layout
When a record crosses the boundary, both sides must agree on every offset. packed record plus the ctypes names is how you get that agreement without either compiler guessing.
#include <stdio.h> #include <stddef.h> #include <stdint.h> /* The shape both sides must agree on. */ #pragma pack(push, 1) struct WireHeader { uint8_t kind; uint32_t length; uint16_t flags; }; #pragma pack(pop) int main(void) { struct WireHeader header = { 7, 1024, 3 }; printf("size %zu\n", sizeof header); printf("kind at %zu\n", offsetof(struct WireHeader, kind)); printf("length at %zu\n", offsetof(struct WireHeader, length)); printf("flags at %zu\n", offsetof(struct WireHeader, flags)); printf("values %u %u %u\n", header.kind, header.length, header.flags); return 0; }
program shared_layout; type TWireHeader = packed record kind: Byte; length: LongWord; flags: Word; end; var header: TWireHeader; bytes: array [0..6] of Byte absolute header; index: Integer; begin header.kind := 7; header.length := 1024; header.flags := 3; writeln('size ', SizeOf(TWireHeader)); writeln('kind at ', PtrUInt(@header.kind) - PtrUInt(@header)); writeln('length at ', PtrUInt(@header.length) - PtrUInt(@header)); writeln('flags at ', PtrUInt(@header.flags) - PtrUInt(@header)); writeln('values ', header.kind, ' ', header.length, ' ', header.flags); write('bytes '); for index := 0 to SizeOf(TWireHeader) - 1 do write(bytes[index], ' '); writeln; end.
Both columns report seven bytes, because both are packed and the fields are 1, 4 and 2 wide. The absolute declaration in the Pascal column overlays a byte array on the record at the same address — a variant record would do the same job — and the bytes it prints are in the machine’s own order, so a wire format still needs explicit byte-swapping. Drop the packed and the size becomes 12 on both sides, for the same alignment reasons.