Processing CSV Files in WebAssembly with Pascal
07-09-2026
From Simple Math to Real-World I/O
In yesterday's post, Your First WebAssembly Module in Pascal with WasmPascal,
we walked through the absolute basics: writing a tiny Pascal library, compiling it to .wasm right in the browser,
and invoking an exported function (Add(a, b: Integer)) from JavaScript. That was intentionally minimal to show
the basics of getting a WebAssembly module up and running with minimun fuss. We're all about as little fuss as possible here!
Of course, real-world software rarely deals only with just two numbers. Soon enough, you need to do actual work: reading files, parsing text, manipulating structured records, sorting collections, and providing downloadable results back to the user.
Which brings up a classic question: "WebAssembly doesn't have an operating system file system in the browser! How do I read a file and write a file?"
The answer lies in understanding the division of labor between the browser host and WebAssembly:
- The Browser Host (HTML & JavaScript) handles user interaction, file system dialogs (via standard
<input type="file">), drag-and-drop, and file downloads (viaBloband Object URLs). - WebAssembly Linear Memory (
ArrayBuffer) acts as a shared bridge where raw file bytes are deposited. - The Pascal Module processes those bytes at compiled machine speed: parsing CSV fields, populating records, running an efficient sorting algorithm, formatting the output, and handing the result back.
In this post, we'll build this end-to-end. We'll write a Pascal library that accepts raw CSV data containing names and ages, parses the lines, sorts everyone by age in ascending order, and constructs a clean, sorted CSV output buffer ready for download. And to make things fun, there's a live working demo embedded right in this article!
Once again, I'll try to keep this as short and simple as possible, but these techniques are what I use in more complicated programs, I'm just going to skip some fluff like deep error trapping, retries etc. Those can of course be added later, but we're going to focus on getting things working so that the foundations make sense.
How WebAssembly File I/O Actually Works
When you run code natively on Windows, macOS, or Linux, your Pascal program might call AssignFile(),
Reset(), or Rewrite(). The OS kernel grants disk access, handles file descriptors, and reads blocks from storage.
WebAssembly inside a web browser has no direct access to your hard drive. This is a crucial security guarantee: arbitrary code running on a web page cannot silently snoop through your local files.
Instead, the browser's JavaScript environment acts as the gatekeeper. When a user picks a file through an HTML file picker:
- JavaScript reads the file contents into memory using the browser's built-in
FileReaderorfile.arrayBuffer()API. - JavaScript asks the Pascal WASM module for its input buffer memory address via an exported function (
GetInputBuffer). - JavaScript copies the bytes directly into the WASM instance's shared memory (
instance.exports.memory.buffer) using a typed array (Uint8Array). - JavaScript calls the exported Pascal routine
ProcessCSV(byteCount). - Pascal parses the records, runs the sort, formats the output into an output buffer, and returns the number of sorted items.
- JavaScript reads the output bytes from WASM memory, wraps them in a
Blobwith MIME typetext/csv, and triggers a browser download.
The magic here is of cours shared linear memory. This allows JavaScript and Wasm to communite across boundaries and share data. Depending on what you are doing, one side writes data to this linear memory while the other one reads from it.
Interactive Live Demo
Here is the completed application running live right inside this page, powered by csvsort.wasm compiled from our Pascal source via WasmPascal:
The Pascal Source: csvsort.pas
Let's inspect how the Pascal module is designed. We declare our unit as a library, which produces a clean
binary exporting only the functions we specify.
First, we define our data structures:
library csvsort;
const
MAX_INPUT = 65536; { 64 KB input & output buffer }
MAX_RECORDS = 512; { Up to 512 people records }
type
TPerson = record
name: String[64];
age: Integer;
end;
var
inputBuffer: array[0..MAX_INPUT - 1] of Char;
outputBuffer: array[0..MAX_INPUT - 1] of Char;
outputLen: Integer;
people: array[0..MAX_RECORDS - 1] of TPerson;
peopleCount: Integer;
Notice what we've done here:
inputBufferandoutputBufferare static arrays reserved in the module's linear data segment. They don't require dynamic heap allocation.TPersonstores a fixed-length stringname: String[64]and an integerage. Fixed-length strings in Pascal are compact and safe: a 1-byte length prefix followed by character bytes.- We define functions to expose memory pointers to JavaScript:
function GetInputBuffer: Integer; begin GetInputBuffer := Integer(@inputBuffer[0]); end; function GetOutputBuffer: Integer; begin GetOutputBuffer := Integer(@outputBuffer[0]); end; function GetOutputLength: Integer; begin GetOutputLength := outputLen; end;
In WebAssembly, a pointer is simply an i32 representing the byte offset in the module's linear memory.
When JavaScript calls GetInputBuffer(), it receives an integer (for example, 0 or 1024),
indicating exactly where in the ArrayBuffer it should write the input bytes.
The Sorting Logic: Classic Insertion Sort
Once the CSV rows are parsed into our people array, we sort them by age ascending.
An insertion sort is fast, stable, and completely self-contained without needing recursion or auxiliary stack frames.
This makes it a good fit for Wasm where we have to manage memory ourselves:
procedure SortPeople;
var
i, j: Integer;
temp: TPerson;
begin
for i := 1 to peopleCount - 1 do
begin
temp := people[i];
j := i - 1;
while (j >= 0) and (people[j].age > temp.age) do
begin
people[j + 1] := people[j];
j := j - 1;
end;
people[j + 1] := temp;
end;
end;
Emitting the Output CSV
Once sorted, we construct the output CSV directly into the outputBuffer:
{ Append CSV header: Name,Age }
AppendChar('N'); AppendChar('a'); AppendChar('m'); AppendChar('e');
AppendChar(',');
AppendChar('A'); AppendChar('g'); AppendChar('e');
AppendChar(#10);
{ Append each sorted person }
for pos := 0 to peopleCount - 1 do
begin
curName := people[pos].name;
for k := 1 to Length(curName) do
AppendChar(curName[k]);
AppendChar(',');
AppendInt(people[pos].age);
AppendChar(#10);
end;
Finally, we publish our API in the exports clause. I like to be explicit about my export names, you don't have to be, it's just my preference:
exports GetInputBuffer name 'GetInputBuffer', GetOutputBuffer name 'GetOutputBuffer', GetOutputLength name 'GetOutputLength', GetMaxInputSize name 'GetMaxInputSize', ProcessCSV name 'ProcessCSV';
Step 4: Compiling with WasmPascal
You can compile this code directly in the browser at wasmpascal.com:
- Click Copy Code in the Full Pascal Source section below.
- Paste the source into the editor at wasmpascal.com.
- Click Run. The Odin-based compiler runs in-browser, parsing and generating the WebAssembly binary in seconds.
- Click Download .wasm and save the file as
csvsort.wasmin your project directory.
The HTML & JavaScript Host: index.html
Now let's examine the companion host page. We need an HTML file input element and a few lines of modern JavaScript.
1. Loading the WebAssembly Module
var wasmInstance = null;
// Standard import object for WasmPascal modules
var importObject = {
wasmpascal_env: {
heap_report: function() {}
}
};
WebAssembly.instantiateStreaming(fetch('csvsort.wasm'), importObject)
.then(function(obj) {
wasmInstance = obj.instance;
console.log('Pascal WASM CSV engine ready!');
});
2. Reading the File and Passing Bytes into WASM
When the user selects a .csv file, we read it using file.arrayBuffer() (or FileReader):
function processFile(file) {
var reader = new FileReader();
reader.onload = function(e) {
var rawText = e.target.result;
runPascalSort(rawText);
};
reader.readAsText(file);
}
3. The Memory Bridge: Writing, Calling, and Reading
This is where the magic happens:
function runPascalSort(csvText) {
var exports = wasmInstance.exports;
var memory = exports.memory;
// 1. Encode JavaScript string into raw UTF-8 bytes
var encoder = new TextEncoder();
var inputBytes = encoder.encode(csvText);
var maxInput = exports.GetMaxInputSize();
if (inputBytes.length > maxInput) {
alert('File is too large! Max supported size is ' + maxInput + ' bytes.');
return;
}
// 2. Write input bytes directly into Pascal linear memory
var inAddr = exports.GetInputBuffer();
var wasmMemoryView = new Uint8Array(memory.buffer);
wasmMemoryView.set(inputBytes, inAddr);
// 3. Execute the Pascal CSV parser and sorter!
var recordCount = exports.ProcessCSV(inputBytes.length);
console.log('Pascal sorted ' + recordCount + ' records');
// 4. Read back the sorted CSV output buffer
var outAddr = exports.GetOutputBuffer();
var outLen = exports.GetOutputLength();
var outputBytes = new Uint8Array(memory.buffer, outAddr, outLen);
var decoder = new TextDecoder('utf-8');
var sortedCSVText = decoder.decode(outputBytes);
// 5. Trigger download!
triggerDownload(sortedCSVText, 'sorted_people.csv');
}
4. Generating the File Download
To provide the file download without a server round-trip, we wrap the string in a standard browser Blob
and create an Object URL:
function triggerDownload(content, filename) {
var blob = new Blob([content], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
var link = document.createElement('a');
link.href = url;
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
}
Detailed Step-by-Step Breakdown
Let's review the critical concepts at work here:
-
Memory Isolation & Safety: The WASM memory is a single contiguous
ArrayBuffer. Pascal cannot access anything in JavaScript memory, and JavaScript cannot access anything outside that buffer. Passing data simply means writing into the buffer and calling a function with the byte offset. - No Serialization Overhead: In JSON-heavy APIs, data is converted from objects to strings, sent across boundaries, parsed into new objects, and then converted back. Here, raw bytes flow directly into pre-allocated struct memory. This is both simpler and, sometimes, more complex. It is, however, quite powerful and you can do really interesting things with it.
-
Handling Real CSV Nuances: Our parser accommodates common CSV variations: carriage returns (
\r\nfrom Windows), trimming quotation marks, skipping blank lines, and auto-detecting/skipping header rows likeName,Age.
Full Pascal Source (csvsort.pas)
Copy this into the WasmPascal editor and hit Run:
library csvsort;
{ ------------------------------------------------------------------ }
{ csvsort.pas — CSV Parsing and Sorting in WebAssembly using Pascal }
{ }
{ Accepts raw CSV bytes (Name,Age) deposited in inputBuffer by JS, }
{ parses records into a TPerson array, sorts them by age ascending, }
{ and generates a sorted CSV in outputBuffer ready for download. }
{ }
{ Compile at https://wasmpascal.com/ -> Run -> Download .wasm }
{ Save as csvsort.wasm alongside companion index.html. }
{ ------------------------------------------------------------------ }
const
MAX_INPUT = 65536; { 64 KB buffer limit }
MAX_RECORDS = 512; { Up to 512 person records }
type
TPerson = record
name: String[64];
age: Integer;
end;
var
inputBuffer: array[0..MAX_INPUT - 1] of Char;
outputBuffer: array[0..MAX_INPUT - 1] of Char;
outputLen: Integer;
people: array[0..MAX_RECORDS - 1] of TPerson;
peopleCount: Integer;
function GetInputBuffer: Integer;
begin
GetInputBuffer := Integer(@inputBuffer[0]);
end;
function GetOutputBuffer: Integer;
begin
GetOutputBuffer := Integer(@outputBuffer[0]);
end;
function GetOutputLength: Integer;
begin
GetOutputLength := outputLen;
end;
function GetMaxInputSize: Integer;
begin
GetMaxInputSize := MAX_INPUT;
end;
procedure AppendChar(c: Char);
begin
if outputLen < MAX_INPUT then
begin
outputBuffer[outputLen] := c;
outputLen := outputLen + 1;
end;
end;
procedure AppendInt(n: Integer);
var
temp: array[0..15] of Char;
digits: Integer;
val: Integer;
d: Integer;
begin
if n = 0 then
begin
AppendChar('0');
Exit;
end;
if n < 0 then
begin
AppendChar('-');
val := -n;
end
else
val := n;
digits := 0;
while val > 0 do
begin
d := val mod 10;
temp[digits] := Chr(Ord('0') + d);
digits := digits + 1;
val := val div 10;
end;
while digits > 0 do
begin
digits := digits - 1;
AppendChar(temp[digits]);
end;
end;
{ Sort people array by age ascending using insertion sort }
procedure SortPeople;
var
i, j: Integer;
temp: TPerson;
begin
for i := 1 to peopleCount - 1 do
begin
temp := people[i];
j := i - 1;
while (j >= 0) and (people[j].age > temp.age) do
begin
people[j + 1] := people[j];
j := j - 1;
end;
people[j + 1] := temp;
end;
end;
{ Main processing entry point: returns number of sorted records }
function ProcessCSV(inputLen: Integer): Integer;
var
pos: Integer;
ch: Char;
curName: String[64];
curAge: Integer;
isFirstLine: Boolean;
hasAge: Boolean;
k: Integer;
begin
peopleCount := 0;
outputLen := 0;
if (inputLen <= 0) or (inputLen > MAX_INPUT) then
begin
ProcessCSV := 0;
Exit;
end;
pos := 0;
isFirstLine := True;
while pos < inputLen do
begin
{ Read name until comma, newline, or end of buffer }
curName := '';
while (pos < inputLen) and (inputBuffer[pos] <> ',') and (inputBuffer[pos] <> #10) and (inputBuffer[pos] <> #13) do
begin
ch := inputBuffer[pos];
if not ((Length(curName) = 0) and ((ch = ' ') or (ch = '"'))) then
begin
if ch <> '"' then
curName := curName + ch;
end;
pos := pos + 1;
end;
{ Skip comma delimiter }
if (pos < inputLen) and (inputBuffer[pos] = ',') then
pos := pos + 1;
{ Read integer age digits until newline }
curAge := 0;
hasAge := False;
while (pos < inputLen) and (inputBuffer[pos] <> #10) and (inputBuffer[pos] <> #13) do
begin
ch := inputBuffer[pos];
if (ch >= '0') and (ch <= '9') then
begin
curAge := curAge * 10 + (Ord(ch) - Ord('0'));
hasAge := True;
end;
pos := pos + 1;
end;
{ Skip carriage return and line feed }
while (pos < inputLen) and ((inputBuffer[pos] = #13) or (inputBuffer[pos] = #10)) do
pos := pos + 1;
{ Skip header line if detected }
if isFirstLine and ((curName = 'Name') or (curName = 'name') or (not hasAge)) then
begin
{ header line skipped }
end
else if (Length(curName) > 0) and hasAge and (peopleCount < MAX_RECORDS) then
begin
people[peopleCount].name := curName;
people[peopleCount].age := curAge;
peopleCount := peopleCount + 1;
end;
isFirstLine := False;
end;
{ Sort records by age ascending }
if peopleCount > 1 then
SortPeople;
{ Format CSV output into outputBuffer }
AppendChar('N'); AppendChar('a'); AppendChar('m'); AppendChar('e');
AppendChar(',');
AppendChar('A'); AppendChar('g'); AppendChar('e');
AppendChar(#10);
for pos := 0 to peopleCount - 1 do
begin
curName := people[pos].name;
for k := 1 to Length(curName) do
AppendChar(curName[k]);
AppendChar(',');
AppendInt(people[pos].age);
AppendChar(#10);
end;
ProcessCSV := peopleCount;
end;
exports
GetInputBuffer name 'GetInputBuffer',
GetOutputBuffer name 'GetOutputBuffer',
GetOutputLength name 'GetOutputLength',
GetMaxInputSize name 'GetMaxInputSize',
ProcessCSV name 'ProcessCSV';
begin
end.
Full HTML & JavaScript Host (index.html)
Save this as index.html in the same directory as csvsort.wasm:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Pascal WebAssembly CSV Sorter</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
max-width: 680px;
margin: 3rem auto;
padding: 0 1rem;
line-height: 1.5;
}
h1 { color: #1f2937; }
.box {
border: 1px solid #d1d5db;
border-radius: 8px;
padding: 1.5rem;
margin-top: 1.5rem;
background: #f9fafb;
}
.controls { display: flex; gap: 0.75rem; align-items: center; margin-bottom: 1rem; flex-wrap: wrap; }
button, .btn {
background: #2563eb;
color: #fff;
border: none;
padding: 8px 16px;
border-radius: 4px;
cursor: pointer;
font-size: 0.95rem;
text-decoration: none;
}
button:hover, .btn:hover { background: #1d4ed8; }
pre {
background: #1e293b;
color: #f8fafc;
padding: 1rem;
border-radius: 6px;
overflow-x: auto;
max-height: 240px;
}
#status { font-size: 0.9rem; color: #4b5563; margin-bottom: 0.5rem; }
</style>
</head>
<body>
<h1>Pascal WebAssembly CSV Sorter</h1>
<p>Upload a CSV file containing <code>Name,Age</code> to sort records by age using compiled Pascal in WASM.</p>
<div class="box">
<div class="controls">
<input type="file" id="fileInput" accept=".csv,text/csv">
<button type="button" id="loadSampleBtn">Load Sample</button>
<button type="button" id="sortBtn" disabled>Sort CSV</button>
<a id="downloadLink" class="btn" style="display:none;" download="sorted_people.csv">Download Sorted CSV</a>
</div>
<div id="status">Loading WebAssembly module…</div>
<h4>Output Preview:</h4>
<pre id="outputView">(No file processed yet)</pre>
</div>
<script>
'use strict';
var wasmInstance = null;
var rawCsvData = '';
var statusEl = document.getElementById('status');
var fileInput = document.getElementById('fileInput');
var sortBtn = document.getElementById('sortBtn');
var loadSampleBtn = document.getElementById('loadSampleBtn');
var outputView = document.getElementById('outputView');
var downloadLink = document.getElementById('downloadLink');
// WasmPascal host environment import
var importObject = {
wasmpascal_env: {
heap_report: function () {}
}
};
// 1. Fetch and compile the WebAssembly binary
WebAssembly.instantiateStreaming(fetch('csvsort.wasm'), importObject)
.then(function (obj) {
wasmInstance = obj.instance;
statusEl.textContent = 'Module ready. Select a CSV file or load sample data.';
})
.catch(function (err) {
statusEl.textContent = 'Failed to load csvsort.wasm: ' + err;
console.error(err);
});
// 2. Handle file selection
fileInput.addEventListener('change', function (e) {
var file = e.target.files[0];
if (!file) return;
var reader = new FileReader();
reader.onload = function (evt) {
rawCsvData = evt.target.result;
statusEl.textContent = 'Loaded "' + file.name + '" (' + rawCsvData.length + ' chars). Click Sort CSV.';
sortBtn.disabled = false;
};
reader.readAsText(file);
});
// Sample data helper
loadSampleBtn.addEventListener('click', function () {
rawCsvData = 'Name,Age\nCharlie,35\nAlice,30\nBob,25\nDiana,22\nEthan,28\nZara,19\n';
statusEl.textContent = 'Sample data loaded (6 records). Click Sort CSV.';
sortBtn.disabled = false;
outputView.textContent = rawCsvData;
});
// 3. Process CSV in Pascal WebAssembly
sortBtn.addEventListener('click', function () {
if (!wasmInstance || !rawCsvData) return;
var exp = wasmInstance.exports;
var encoder = new TextEncoder();
var inputBytes = encoder.encode(rawCsvData);
var maxInput = exp.GetMaxInputSize();
if (inputBytes.length > maxInput) {
alert('Data size exceeds maximum allowed (' + maxInput + ' bytes)');
return;
}
// Write bytes directly into Pascal memory at GetInputBuffer address
var inAddr = exp.GetInputBuffer();
var memView = new Uint8Array(exp.memory.buffer);
memView.set(inputBytes, inAddr);
// Call the Pascal sorting engine
var count = exp.ProcessCSV(inputBytes.length);
// Read back the sorted output buffer
var outAddr = exp.GetOutputBuffer();
var outLen = exp.GetOutputLength();
var outBytes = new Uint8Array(exp.memory.buffer, outAddr, outLen);
var decoder = new TextDecoder('utf-8');
var sortedCsv = decoder.decode(outBytes);
statusEl.textContent = 'Successfully sorted ' + count + ' records by age!';
outputView.textContent = sortedCsv;
// Prepare downloadable Blob
var blob = new Blob([sortedCsv], { type: 'text/csv;charset=utf-8;' });
var url = URL.createObjectURL(blob);
downloadLink.href = url;
downloadLink.style.display = 'inline-block';
});
</script>
</body>
</html>
Wrapping Up
Handling files in WebAssembly is all about letting the browser handle user security, disk access, and download triggers, while delegating high-performance data crunching to compiled Pascal in WebAssembly through shared linear memory.
With just a few Pascal routines and around 50 lines of JavaScript, you have a fast, zero-dependency CSV processing pipeline that runs entirely on the client's machine without any backend servers.
If you haven't checked it out yet, give wasmpascal.com a spin, and check out And then there was Pascal, on the web! for more behind-the-scenes insights on compiling Pascal to WebAssembly.