Foreign Function Interface (FFI)

Argentum can call C/C++ functions and pass parameters of primitive data types and objects.

On Argentum side:

Create a module io.ag and declare there a number of functions with ";" in place of body:

using sys { String, Cursor, Blob }
readFile(name str, content Blob) int;
writeFile(name str, at int, byteCount int, content -Blob) bool;
On C/C++ side:

Create a library:

cmake_minimum_required (VERSION 3.8)
set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
project ("io_lib")
add_library (io_lib STATIC
	"io_lib.c"
	"../argentum/src/runtime/runtime.h")

If in C++, this function should be declared in extern "C" statement. In C this is not needed.

Functions must be nameded "ag_fn_<moduleName>_<functionName>".

runtime.h - contains declarations of C99 helper functions and Argentum data structures. Our example uses Blob and String objects.

#include <stdio.h>
#include "../argentum/src/runtime/runtime.h"

int64_t ag_fn_io_readFile(AgString* name, AgBlob* content) {
	FILE* f = fopen(name->ptr, "rb");  // name->ptr - points to zero-terminated utf8 chars
	if (!f)
		return -1;
	fseek(f, 0, SEEK_END);
	int64_t size = ftell(f);
	fseek(f, 0, SEEK_SET);
	ag_make_blob_fit(content, size);  // helper that resizes sys_Blob
	int64_t read_size = fread(content->data, 1, size, f);  // content->data points to the actual buffer
	fclose(f);
	return read_size == size ? size : -1;
}

bool ag_fn_io_writeFile(AgString* name, int64_t at, int64_t size_in_bytes, AgBlob* content) {
	if (at < 0
		|| (uint64_t)at >= content->size * sizeof(int64_t)
		|| (uint64_t)at + (uint64_t)size_in_bytes > content->size * sizeof(int64_t))
		return -1;
	FILE* f = fopen(name->ptr, "wb");
	if (!f)
		return -1;
	int64_t size_written = fwrite((char*)content->data + at, 1, size_in_bytes, f);
	fclose(f);
	return size_written == size_in_bytes;
}

Build C99 library and copy it from the "out\build\x64-Release\io_lib.lib"
to the "argentum-demo\libs\io_lib.lib"

We also need to add our new library to the linker invocation:

...... lld-link ..... "..\libs\io_lib.lib"

For the sake of simplicity I'm going to put all tutorial code to the io.ag module.

That's it. Now we can read and write binary files:

data = Blob;
dataSizeInBytes = readFile("1.txt", data);
dataSizeInBytes >= 0 && writeFile("1.copy.txt", 0, dataSizeInBytes, data)
   ?  log("success");
Support for Text files
fn readTextFile(name str) ?str {       // {1}
    data = Blob;
    size = readFile(name, data);             // {2}
    size >= 0                                // {3}
       ? data.mkStr(0, size)                 // {4}
}

Where:

  • In line {1} we declare that our function returns ?str aka "optional immutable String" type. In can have either nothing or a text string
  • Line {2} contains the actual call to our C99 function
  • Lines {3,4} is a single statement, that was wrapped on two lines to add comments
  • Line {3} checks the result of the read operation.
  • Line {4} calls the Argentum RTL library function that creates an immutable string from a portion of the mutable byte buffer
  • Line {4} contains a "?"-operator, that takes a condition to the left and an expression to the right (in this example - "r").
    • If condition fulfills, the "?"-operator executes its right-hand-side (RHS) expression (that in this example returns string) and wraps its result in the optional string type.
    • Otherwise it returns an empty optional string.

Let's make the main program:

log(readTextFile("1.txt") : "no data");

In this code we call a readTextFile function, and check its optional(String) result against "emptiness".

The binary operator ":" expects optional(String) to the left, and a String expression to the right.

  • If LHS result is not empty, ":"-operator returns it's extracted (i.e. it's no more optional) value
  • otherwise, ":"-operator the executes its RHS to make a value.

If we execute this program, we'll see "no data" in the console.

Let's create the "1.txt" file in the "argentum-demo\workdir\" directory:

(Donald Duck),(Mickey Mouse)

When we run it again, the file contents will be printed to the stdout.

Read-parse-convert-write data

First of all, let's extends our sys_Cursor class with one helper method:

class Cursor {                       // {1}
    getTill(delimiter int) str {  // {2}
        r = StrBuilder;              // {3}
        loop !{                      // {4}
            c = get();               // {5}
            c != delimiter && c != 0 ? r.putCh(c)  // {6}
        };
        r.toStr()                    // {7}
    }
}

This code fragment "extends" the sys_Cursor class in line {1} and adds a getTill method to it in line {2}.

This method extracts a portion of the string either to the delimiter or to the end of string:

  • in a loop (line {4})
  • it reads a Unicode codepoint out of string (line{5})
  • and if it is not a delimiter and not a end-of-string, it appends this character {6} to the string builder created at the line {3}
  • In general the loop operator iterates till its body returns "empty optional" value (so it acts as repeat-until). In this example we use negate "!" operator to make our loop a "repeat-while".
  • Line {7} extracts a string out of string builder.

Having this method we can write the following file parsing code:

b = StrBuilder;
readTextFile("1.txt") ? _.cursor() -> {  // {1}
    loop {
        _.getTill('(');              // {2}
        firstName = _.getTill(' ');  // {3}
        lastName = _.getTill(')');   // {4}
        b.putStr("First name=").putStr(firstName).newLine();     // {5}
        b.putStr("Last name=").putStr(lastName).putStr("\n\n");  // {6}
        _.get() == 0                                             // {7}
    };
    writeFile("2.txt", 0, b.pos, b) ? log("success\n")           // {8}
}

Where:

  • Line {1} reads file and skips all processing on failure. On success the file content is passed to the RHS expression of the "?"-operator as a "_" variable, where we extract a cursor to scans this string start-to end and pass this cursor in the {}-block.
  • Line{2} skips everything till "(" using our newly defined Cursor method.
  • Lines {3} and {4} extract pieces of string into variables.
  • Lines {5} and {6} format them in a new way.
  • Line {7} continues the loop skipping "," and checking for the end of file.
  • Line {8} calls our C99 function writeFile and prints "success" to the console if it returns true.

Lines 3..6 can be rewritten using multiline string literals and string interpolation:

        b.putStr("{}//
           First name={_.getTill(' ')}
           Last name={_.getTill(')')}
        ");

If we execute this program, we'll see "success" in the console. And file "2.txt" in the workdir:

First name=Donald
Last name=Duck

First name=Mickey
Last name=Mouse

The full code of this tutorial:

using sys { String, StrBuilder, Blob, log }
using string;

fn readFile(name -String, content Blob) int;
fn writeFile(name -String, at int, byteCount int, content -Blob) bool;

fn readTextFile(name String) ?String {
    r = "";
    data = Blob;
    size = readFile(name, data);
    size >= 0 && r.fromBlob(data, 0, size) ? r
}

class String {
    getTill(delimiter int) String {
        r = StrBuilder;
        loop !{
            c = get();
            c != delimiter && c != 0 ? r.putCh(c)
        };
        r.toStr()
    }
}

readTextFile("1.txt") ? {
    b = StrBuilder;
    loop {
        _.getTill('(');
        x = _.getTill(' ');
        y = _.getTill(')')
        b.putStr("{}//
           First name={x}
           Last name={y}
        ");
        _.get() == 0
    };
    writeFile("2.txt", 0, b.pos, b) ? log("success\n")
}

This tutorial contains:

  • Bindings to C99
  • More on optional values and "?" ":" operators.
  • Class extensions.
  • Loops

3 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *