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:

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:

  1. JavaScript reads the file contents into memory using the browser's built-in FileReader or file.arrayBuffer() API.
  2. JavaScript asks the Pascal WASM module for its input buffer memory address via an exported function (GetInputBuffer).
  3. JavaScript copies the bytes directly into the WASM instance's shared memory (instance.exports.memory.buffer) using a typed array (Uint8Array).
  4. JavaScript calls the exported Pascal routine ProcessCSV(byteCount).
  5. Pascal parses the records, runs the sort, formats the output into an output buffer, and returns the number of sorted items.
  6. JavaScript reads the output bytes from WASM memory, wraps them in a Blob with MIME type text/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:

Pascal CSV Age Sorter (WASM Live Demo)

Status: Loading WebAssembly module…
#NameAge
No data processed yet

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:

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:

  1. Click Copy Code in the Full Pascal Source section below.
  2. Paste the source into the editor at wasmpascal.com.
  3. Click Run. The Odin-based compiler runs in-browser, parsing and generating the WebAssembly binary in seconds.
  4. Click Download .wasm and save the file as csvsort.wasm in 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:


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&hellip;</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.