Back to Bend

Dynamically linked libraries and foreign functions

docs/ffi.md

0.2.387.7 KB
Original Source

Dynamically linked libraries and foreign functions

We can add new IO functions to Bend during runtime by loading dynamic libraries.

Using IO dynamic libraries in Bend

Here is an example of how we could load a Bend library that contains functions for working with directories.

py
def main():
  with IO:
    # Open the dynamic library file
    # The second argument is '0' if we want to load all functions immediately.
    # Otherwise it should be '1' when we want to load functions as we use them.
    # 'dl' is the unique id of the dynamic library.
    dl <- IO/DyLib/open("./libbend_dirs.so", 0)

    # We can now call functions from the dynamic library.
    # We need to know what functions are available in the dynamic library.
    # If you're writing a library for Bend that uses a dynamically linked library
    # you should wrap the IO calls so that users don't need to know what's in the dynamic library.

    # The first argument is the dynamic library id.
    # The second argument is the name of the function we want to call as a String.
    # The third argument are the arguments to the function.
    # You need to know the types of the arguments and the return type of the function.

    # In our example, 'ls' receives a path as a String and
    # returns a String with the result of the 'ls' command.
    unwrapped_dl = Result/unwrap(dl)
    files_bytes <- IO/DyLib/call(unwrapped_dl, "ls", "./") 
    files_str = String/decode_utf8(Result/unwrap(files_bytes)) 
    files = String/split(files_str, '\n')

    # We want to create a directory for a new user "my_user" if it doesn't exist.
    my_dir = List/filter(files, String/equals("my_dir"))
    match my_dir:
      case List/Cons:
        # The directory already exists, do nothing.
        * <- IO/print("Directory already exists.\n")
        status = wrap(-1)
      case List/Nil:
        # The directory doesn't exist, create it.
        * <- IO/DyLib/call(unwrapped_dl, "mkdir", "./my_dir")
        * <- IO/print("Directory created.\n")
        status = wrap(+0)
    status <- status

    # Here the program ends so we didn't need to close the dynamic library,
    # but it's good practice to do so once we know we won't need it anymore.
    * <- IO/DyLib/close(unwrapped_dl)
    return wrap(status)

Writing IO dynamic libraries for Bend

Bend IO libraries need to be implemented in C or Cuda (depending on the backend you're targeting) using the HVM API.

Writing libraries for the C runtime

The functions you call from Bend using IO/DyLib/call must have the following signature:

c
Port function_name(Net* net, Book* book, Port arg);

Where:

  • net is a pointer to the current network state.
  • book is a pointer to the book of function definitions.
  • arg is a pointer to the arguments of the function.

The return value must be a Port that points to the return value of the function.

HVM provides some util functions to do the conversions from HVM to C and vice versa, so that you don't need to understand the details of the HVM runtime.

We can implement the example library from earlier for the C runtime with the following C code:

c
// This is a header file that contains the HVM API.
#include "hvm.h"

// The headers we need to open and read directories.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

// IO functions must have this exact signature.
// The first argument is a pointer to the graph with the current state of the program.
// The second argument is a pointer to the book of function definitions.
// The third argument points to the arguments of the function.
// The return value must be a port that points to the return value of the function.
Port ls(Net* net, Book* book, Port arg) {
  // The arguments first need to be converted from HVM to C.
  // For the 'ls' function, this is just a single string.
  Str path = readback_str(net, book, arg);


  // Now we can do the actual IO operations.
  // In this case, we list the contents of the directory
  // by calling the 'ls' program as a subprocess.
  char* cmd = malloc(path.len + strlen("ls ") + 1);
  sprintf(cmd, "ls %s", path.buf);
  free(path.buf);

  FILE* pipe = popen(cmd, "r");
  if (pipe == NULL) {
    // It'd be best practice to return a Result type instead of a null value (ERA).
    // If this command fails and the calling Bend program tries to use the result,
    // it will get corrupted and spit out garbage.
    fprintf(stderr, "failed to run command '%s': %s\n", cmd, strerror(errno));
    return new_port(ERA, 0);
  }
  char buffer[512];
  Bytes output = { .buf = NULL, .len = 0 };
  while (fgets(buffer, sizeof(buffer), pipe) != NULL) {
    size_t len = strlen(buffer);
    char* new_result = realloc(output.buf, output.len + len + 1);
    if (new_result == NULL) {
      fprintf(stderr, "failed to allocate space for output of '%s': %s\n", cmd, strerror(errno));
      free(cmd);
      free(output.buf);
      pclose(pipe);
      return new_port(ERA, 0);
    }
    output.buf = new_result;
    strcpy(output.buf + output.len, buffer);
    output.len += len;
  }

  // After we're done with the operation, we convert it to HVM format.
  // In this case, the output is the output of the 'ls' command as a list of bytes.
  // We need to process it in Bend later to convert it to a list of file names.
  Port output_port = inject_bytes(net, &output);

  // Remember to free all the allocated memory.
  free(cmd);
  free(output.buf);
  pclose(pipe);
  return output_port;
}

Port mkdir(Net* net, Book* book, Port arg) {
  // We do the same thing here as in the 'ls' function,
  // except we call 'mkdir' which doesn't output anything.
  Str path = readback_str(net, book, arg);

  char* cmd = malloc(path.len + strlen("mkdir ") + 1);
  sprintf(cmd, "mkdir %s", path.buf);
  int res = system(cmd);

  free(path.buf);
  free(cmd);
  return new_port(ERA, 0);
}

To compile this code into a library, we can use the gcc compiler and include the HVM header files.

Assuming that it's saved in a file called libbend_dirs.c, we can compile it with the following command:

sh
# Needs to be compiled as a shared library with unresolved symbols.
# For macOS:
gcc -shared -o libbend_dirs.so -I /path/to/HVM/src/ libbend_dirs.c -undefined dynamic_lookup -fPIC

# For Linux:
gcc -shared -o libbend_dirs.so -I /path/to/HVM/src/ libbend_dirs.c -Wl,--unresolved-symbols=ignore-all -fPIC

Now we can use the dynamic library in our Bend program, we just need to pass the path to the library to IO/DyLib/open.

Writing libraries for the Cuda runtime

Writing libraries for the Cuda runtime is very similar to writing libraries for the C runtime.

The main difference is the function signature:

c++
Port function_name(GNet* gnet, Port argm)

Where:

  • gnet is a pointer to the current network state.
  • argm is the argument to the function.

The return value must be a Port that points to the return value of the function.

To compile libraries of the Cuda runtime, we can use the nvcc compiler and include the HVM header files.

Assuming that it's saved in a file called libbend_dirs.cu, we can compile it with the following command:

sh
nvcc -shared -o libbend_dirs.so -I /path/to/hvm/ libbend_dirs.cu

Compiling Bend programs that use dynamic libraries

To compile the C or Cuda program generated from a Bend program that uses dynamic libraries, we need to use the -rdynamic flag to allow the dynamic library to use symbols from the main program.

For example, if we have a Bend program called main.bend that uses the dynamic library libbend_dirs.so, we need compile to it with the following commands:

sh
# Compiling for C
bend gen-c my_app.bend > my_app.c
gcc -rdynamic -lm my_app.c -o my_app

# Compiling for Cuda
bend gen-cu my_app.bend > my_app.cu
nvcc --compiler-options=-rdynamic my_app.cu -o my_app