Package: src/packages/dynlink.fdoc

Dynamic Linker

key file
flx_dl.h share/lib/rtl/flx_dl.h
flx_dlopen.hpp share/lib/rtl/flx_dlopen.hpp
flx_dlopen.cpp share/src/dynlink/flx_dlopen.cpp
flx_dynlink.hpp share/lib/rtl/flx_dynlink.hpp
flx_dynlink.cpp share/src/dynlink/flx_dynlink.cpp
dynlink.flx share/lib/std/program/dynlink.flx
config_unix_dl.fpc $PWD/src/config/unix/dl.fpc
config_macosx_dl.fpc $PWD/src/config/macosx/dl.fpc
config_win_dl.fpc $PWD/src/config/win/dl.fpc
unix_flx_dynlink.fpc $PWD/src/config/unix/flx_dynlink.fpc
win_flx_dynlink.fpc $PWD/src/config/win/flx_dynlink.fpc
flx_dynlink_config.hpp share/lib/rtl/flx_dynlink_config.hpp
flx_dynlink.py $PWD/buildsystem/flx_dynlink.py

Dynamic Linkage

Synopsis

This subsystem provides the ability to load,link or otherwise access program code at run time. We use the name DLL to refer to a dynamically loaded file containing executable instructions, on Windows this is a dynamic link library which usually ends in extension .dll whilst on Linux we have shared libraries with extension .so and on OSX we use files with extension .dylib.

There is a confusing array of operations provided here which will require refactoring in the future.

The core platform dependent operations are implemented in C++ and configuration and compile time choices determine the platform supported.

These core operations are wrapped, in C++, to remove the file loading dependencies, and provide resource control integrated with the garbage collector.

We use LoadLibrary on Windows and dlopen on Unix platforms wrapped inside a C++ class flx_dynlink_t that represents a library, in Felix the type flx_library is used.

Felix generated code does not permit variables to be stored in static storage. Instead, a structure is used to contain Felix top level variables. For historical reasons objects are called <em>thread frames.</em>

Members of a thread frame are accessed in Felix bindings to C++ using the macro PTF which stands for <em>pointer to thread frame.</em>

A Felix generated shared library requires an instance to be created which is a closure: a pair consisting of the library code and a thread_frame_t object which is allocated on the heap. The closure object has the type flx_libinst_t in C++ and flx_instance in Felix.

Instances require a fixed protocol which involves the library containing exported symbols which can be linked using LoadProcAddress on Windows or dlsym on unix, which can be used to construct the required thread frame. High level Felix functions require he thread frame because it contains a pointer to the garbage collector which in turn provides the system allocator.

Higher level abstractions require more fixed symbols. In particular, there is a protocol for loading a special kind of library called a <em>plugin</em> which make separate compilation of and use of dynamic libraries particularly convenient.

Felix level dynamic loader system

This is a higher level loader which is primarily designed for loading Felix programs machined as libraries, but it can also be used for high level libraries such as plugins.

The core concept is based on Windows 3.1, in which the library is read only program code, and requires an data frame to execute. Unlike C style libraries, mutable data is not permitted in libraries. Instead, the library must provide a function to create a heap allocated data frame to store global data.

Hence, after loading, one or more instances of the library can be created by combining the code API with a data frame. Felix calls this data frame the <em>thread frame</em>.

Since each client of a library create their own instance of the library, the global variables of the client do not interfere.

The type flx_dynlink_t represents a library, whereas the type flx_libinst_t represents a pair consisting of the library together with a data frame. This provides a single entity from which to dispatch function calls which may interact by per instance data without clobbering an independent client’s use of the library.

Except in special circumstances Felix demands all code be reentrant and in particular mutable global variables are not allowed at the C level.

The special circumstances are dictated by poor quality API’s including Posix signals and of course the notorious errno.

//[flx_dynlink.hpp]
#ifndef __FLX_DYNLINK_H__
#define __FLX_DYNLINK_H__
#include "flx_rtl.hpp"
#include "flx_gc.hpp"
#include "flx_dl.h"
#include "flx_dlopen.hpp"
#include "flx_exceptions.hpp"
#include "flx_continuation.hpp"

#include <string>

namespace flx { namespace dynlink {

struct DYNLINK_EXTERN flx_dynlink_t;
struct DYNLINK_EXTERN flx_libinst_t;


/// frame creators.
typedef void *(*thread_frame_creator_t)
(
  ::flx::gc::generic::gc_profile_t*,
  void*
);

/// library initialisation routine.
typedef ::flx::rtl::con_t *(*start_t)
(
  void*,
  int,
  char **,
  FILE*,
  FILE*,
  FILE*

);

typedef ::flx::rtl::con_t *(*main_t)(void*);

/// dynamic object loader.
struct DYNLINK_EXTERN flx_dynlink_t
{
  // filename of library used for dynamic linkage
  ::std::string filename;

  // modulename of library
  // usually filename without path prefix or extension
  ::std::string modulename;

  // OS specific handle refering to the library if one is loaded
  // undefine otherwise
  FLX_LIBHANDLE library;

  // Felix specific entry point used to create thread frame.
  // Typically this function allocates the thread frame as a C++
  // object, calling its contructor.
  // A library together with a thread frame is known as an instance
  // of the library.
  thread_frame_creator_t thread_frame_creator;

  // Felix specific entry point used to initialise thread frame
  // Morally equivalent to the body of a C++ constructor,
  // this calls the libraries initialisation routine.
  // If the library is meant to be a program, this routine
  // often contains the program code.
  start_t start_sym;

  // A separate mainline, morally equivalent to C main() function.
  // Intended to be called after the start routine has completed.
  main_t main_sym;

  // Allow a default initialised default object refering to no library.
  flx_dynlink_t(bool debug);

  // set static link data into an empty dynlink object.
  void static_link(
    ::std::string modulename,
    thread_frame_creator_t thread_frame_creator,
    start_t start_sym,
    main_t main_sym);


  // initialise for static link
  // equivalent to default object followed by call to static_link method
  flx_dynlink_t(
    ::std::string modulename,
    thread_frame_creator_t thread_frame_creator,
    start_t start_sym,
    main_t main_sym,
    bool debug
  ) throw(::flx::rtl::flx_link_failure_t);

  // dynamic link library from filename and module name
  void dynamic_link_with_modulename(
     const ::std::string& filename,
     const ::std::string& modulename) throw(::flx::rtl::flx_link_failure_t);

  // With this variant the module name is calculated from the filename.
  void dynamic_link(const ::std::string& filename) throw(::flx::rtl::flx_link_failure_t);

  virtual ~flx_dynlink_t();

  bool debug;


private:
  void unlink(); // implementation of destructor only
  flx_dynlink_t(flx_dynlink_t const&); // uncopyable
  void operator=(flx_dynlink_t const&); // uncopyable
};

/// Thread Frame Initialisation.

struct DYNLINK_EXTERN flx_libinst_t
{
  void *thread_frame;
  ::flx::rtl::con_t *start_proc;
  ::flx::rtl::con_t *main_proc;
  flx_dynlink_t *lib;
  ::flx::gc::generic::gc_profile_t *gcp;
  void *world;  // FIXME: flx_world*, can't specify atm due to circularity
  bool debug;

  void create
  (
    flx_dynlink_t *lib_a,
    ::flx::gc::generic::gc_profile_t *gcp_a,
    void *world_a, // FIXME as above
    int argc,
    char **argv,
    FILE *stdin_,
    FILE *stdout_,
    FILE *stderr_,
    bool debug_
  );

  void destroy ();

  ::flx::rtl::con_t *bind_proc(void *fn, void *data);
  virtual ~flx_libinst_t();
  flx_libinst_t(bool debug);

private:
  flx_libinst_t(flx_libinst_t const&);
  void operator=(flx_libinst_t const&);
};

DYNLINK_EXTERN extern ::flx::gc::generic::gc_shape_t flx_dynlink_ptr_map;
DYNLINK_EXTERN extern ::flx::gc::generic::gc_shape_t flx_libinst_ptr_map;

}} // namespaces
#endif

Higher level wrappers for finding Felix functions.

Here make a set of higher level wrappers for finding standard protocol Felix function in DLLs. These wrappers create a closure by binding the C address of the constructor for the Felix function class in C++ to the library instance, and return that.

Closures returned by these function can be invoked as normal Felix functions and procedures. Whereas a function defined in the current files binds to the thread frame implicitly, with a library the instance is required to supply the thread frame. The closures returned by these wrappers are bound to the libraries thread frame so they can be invoked with the ordinary syntax.

Note that these operations are not type safe. If you get the type wrong all hell will break loose. This is because dlsym finds functions by their C names and C++ entities use mangled names we cannot compute in a portable way.

//[dynlink.flx]
  //$ Return a closure representing a symbol in a DLL instance
  //$ of a function of no arguments.
  noinline fun func0[R] (linst: flx_instance, sym:string) = {
    var s,tf= flx_dlsym[address --> R] (linst, sym);
    return fun () => s tf;
  }

  //$ Return a closure representing a symbol in a DLL instance
  //$ of a function of one argument.
  noinline fun func1[R,A0] (linst: flx_instance, sym:string) = {
    var s,tf= flx_dlsym[address * A0 --> R] (linst, sym);
    return fun (a0:A0) => s (tf, a0);
  }

  //$ Return a closure representing a symbol in a DLL instance
  //$ of a function of two arguments.
  noinline fun func2[R,A0,A1] (linst: flx_instance, sym:string) = {
    var s,tf= flx_dlsym[address * A0 * A1 --> R] (linst, sym);
    return fun (var a0:A0, var a1:A1) => s (tf, a0, a1);
  }

  //$ Return a closure representing a symbol in a DLL instance
  //$ of a procedure of no arguments.
  noinline fun proc0 (linst: flx_instance, sym:string) = {
    var s,tf= flx_dlsym[address --> void] (linst, sym);
    return proc () { s tf; };
  }

  //$ Return a closure representing a symbol in a DLL instance
  //$ of a procedure of one argument.
  noinline fun proc1[A0] (linst: flx_instance, sym:string) = {
    var s,tf= flx_dlsym[address * A0 --> void] (linst, sym);
    return proc (a0:A0) { s (tf, a0); };
  }

  //$ Return a closure representing a symbol in a DLL instance
  //$ of a procedure of two arguments.
  noinline fun proc2[A0,A1] (linst: flx_instance, sym:string) = {
    var s,tf= flx_dlsym[address * A0 * A1 --> void] (linst, sym);
    return proc (a0:A0,a1:A1) { s (tf, a0, a1); };
  }

Plugins.

A plugin is a special kind of DLL which supplies two fixed entry points: a setup routine, which is called to initialise the thread frame given a string argument, and a single entry point which is subsequently called and which typically returns an object type consisting of a set of methods acting on the object state and initialised thread frame context.

The setup routine typically take a string of configuration parameters, extracts them with a parser, and stores them in variables.

The current protocol is that the setup function must be called “dllname_setup”, the entry point name is passed as a string.

In order to accomodate static linking of plugins in the future, the setup and entry point symbols would need to have univerally unique names, since static linkage cannot work with duplicate definitions, so the protocol will change to require the library name as a prefix. Stay tuned.

//[dynlink.flx]
  //$ Specialised routine(s) to load stylised plugin.
  //$ Two entry points:
  //$
  //$ setup: string -> int
  //$
  //$ is called to initialise the instance globals.
  //$
  //$ entry-point: arg -> iftype
  //$
  //$ is the primary entry point, typically an object factory,
  //$ when called with an argument
  //$ of type arg_t it returns //$ an object of type iftype.
  //$
  //$ This function returns the object factory.
  //$ setup is called automatically with the supplied string.
  //$
  //$ There are 3 variants where the factory function accepts
  //$ 0, 1 and 2 arguments.
  noinline gen  load-plugin-func0[iftype] (
    dll-name: string,   // name of the DLL minus the extension
    setup-str: string="",  // string to pass to setup
    entry-point: string=""   // export name of factory function
  ) : unit -> iftype =
  {
    var entrypoint = if entry-point == "" then dll-name else entry-point;
    var linst = Dynlink::init_lib(dll-name);
    var sresult = Dynlink::func1[int,string] (linst, dll-name+"_setup") (setup-str);
    C_hack::ignore(sresult);
    if sresult != 0 call eprintln$ "[dynlink] Warning: Plugin Library " + dll-name + " set up returned " + str sresult;
    return Dynlink::func0[iftype] (linst, entrypoint);
  }

  noinline gen  load-plugin-func1[iftype, arg_t] (
    dll-name: string,   // name of the DLL minus the extension
    setup-str: string="",  // string to pass to setup
    entry-point: string=""   // export name of factory function
  ) : arg_t -> iftype =
  {
    var entrypoint = if entry-point == "" then dll-name else entry-point;
    var linst = Dynlink::init_lib(dll-name);
    var sresult = Dynlink::func1[int,string] (linst, dll-name+"_setup") (setup-str);
    C_hack::ignore(sresult);
    if sresult != 0 call eprintln$ "[dynlink] Warning: Plugin Library " + dll-name + " set up returned " + str sresult;
    return Dynlink::func1[iftype,arg_t] (linst, entrypoint);
  }

  noinline gen  load-plugin-func2[iftype, arg1_t, arg2_t] (
    dll-name: string,   // name of the DLL minus the extension
    setup-str: string="",  // string to pass to setup
    entry-point: string=""   // export name of factory function
  ) : arg1_t * arg2_t -> iftype =
  {
    var entrypoint = if entry-point == "" then dll-name else entry-point;
    var linst = Dynlink::init_lib(dll-name);
    var sresult = Dynlink::func1[int,string] (linst, dll-name+"_setup") (setup-str);
    C_hack::ignore(sresult);
    if sresult != 0 call eprintln$ "[dynlink] Warning: Plugin Library " + dll-name + " set up returned " + str sresult;
    return Dynlink::func2[iftype,arg1_t, arg2_t] (linst, entrypoint);
  }

Utilities and misc.

//[dynlink.flx]

  //$ Execute an address representing a top
  //$ level exported felix procedure's C wrapper,
  //$ this creates a 'read to run' continuation object
  //$ by both constructing the object using the thread
  //$ frame of the instance as an argument, and calling
  //$ it to fix a null return address and an arbitrary
  //$ client data pointer as arguments to the call method.
  fun bind_proc: flx_instance * address * address -> cont =
    "$1->bind_proc($2,$3)";

  //$ Get the OS dependent handle representing a loaded DLL.
  //$ Return as an address.
  fun dlib_of : flx_library -> address = "(void*)$1->library";

  //$ Throw an exception indicating the failure to
  //$ find a symbol in a DLL.
  proc dlsym_err:flx_library*string="""
    throw ::flx::rtl::flx_link_failure_t($1->filename,$2,"symbol not found");
  """;

  //$ Run a procedure represented by a string name with
  //$ given thread frame.
  noinline proc run_proc (linstance:flx_instance, p: string, data: address)
  {
    var lib = get_library linstance;
    var sym = find_sym(lib, p);
    if isNULL(sym) call dlsym_err(lib,p);
    var f = bind_proc(linstance, sym, data);
    run f;
  }


}

Dynamic Linkage support

//[flx_dynlink_config.hpp]
#ifndef __FLX_DYNLINK_CONFIG_H__
#define __FLX_DYNLINK_CONFIG_H__
#include "flx_rtl_config.hpp"
#ifdef BUILD_DYNLINK
#define DYNLINK_EXTERN FLX_EXPORT
#else
#define DYNLINK_EXTERN FLX_IMPORT
#endif
#endif
//[config_unix_dl.fpc]
Name: dl
Description: dynamic loading support
includes: '<dlfcn.h>'
requires_dlibs: -ldl
requires_slibs: -ldl
//[config_macosx_dl.fpc]
Name: dl
Description: dynamic loading support
includes: '<dlfcn.h>'
//[config_win_dl.fpc]
Name: dl
Description: dynamic loading support
//[unix_flx_dynlink.fpc]
Name: flx_dynlink
Description: Felix Dynamic loading support
provides_dlib: -lflx_dynlink_dynamic
provides_slib: -lflx_dynlink_static
Requires: dl flx_exceptions flx_gc flx_strutil
library: flx_dynlink
includes: '"flx_dynlink.hpp"'
macros: BUILD_DYNLINK
srcdir: src/dynlink
src: .*\.cpp
//[win_flx_dynlink.fpc]
Name: flx_dynlink
Description: Felix Dynamic loading support
provides_dlib: /DEFAULTLIB:flx_dynlink_dynamic
provides_slib: /DEFAULTLIB:flx_dynlink_static
Requires: dl flx_exceptions flx_gc flx_strutil
library: flx_dynlink
includes: '"flx_dynlink.hpp"'
macros: BUILD_DYNLINK
srcdir: src/dynlink
src: .*\.cpp
#[flx_dynlink.py]
import fbuild
from fbuild.path import Path
from fbuild.record import Record
from fbuild.builders.file import copy
from fbuild.functools import call

import buildsystem

# ------------------------------------------------------------------------------

def build_runtime(phase):
    print('[fbuild] [rtl] build dynlink')
    path = Path(phase.ctx.buildroot/'share'/'src/dynlink')

    srcs = [f for f in Path.glob(path / '*.cpp')]
    includes = [phase.ctx.buildroot / 'host/lib/rtl', phase.ctx.buildroot / 'share/lib/rtl']
    macros = ['BUILD_DYNLINK']
    libs = [
        call('buildsystem.flx_strutil.build_runtime', phase),
        call('buildsystem.flx_gc.build_runtime', phase),
    ]

    dst = 'host/lib/rtl/flx_dynlink'
    return Record(
        static=buildsystem.build_cxx_static_lib(phase, dst, srcs,
            includes=includes,
            libs=[lib.static for lib in libs],
            macros=macros),
        shared=buildsystem.build_cxx_shared_lib(phase, dst, srcs,
            includes=includes,
            libs=[lib.shared for lib in libs],
            macros=macros))