Skip to content
/ ufw Public

A minimalist framework for rapid server side applications prototyping in C++ with dependency injection support.

License

Notifications You must be signed in to change notification settings

mrbald/ufw

Repository files navigation

μFW - Micro Framework

build Join the chat at https://gitter.im/mrbald-ufw/Lobby Licence -

μFW is a minimalist framework for rapid server side applications prototyping and experimental work on Unix-like operating systems, primarily Linux and macOS. Those familiar with Spring or Guice may experience a strong deja-vu — dependency injection support was one of the →

Design Objectives

  • Minimum number of lines of code — clean concepts, compact implementations
  • Modularity in spirit of inversion of control
  • Late binding support via shared library-based plugins
  • Consistency in module configuration, lifecycle, concurrency, and logging
  • Singleton-free design with traceable dependencies
  • Structured configuration reflecting both the application topology and the concurrency model
  • Zero steady state runtime overhead
  • Hacking-friendly design

Following core C++ design principles, the rule "you don't pay for what you don't use" is adhered to where possible.

Introduction

uFW Topology

Terminology

The terminology used in the framework maps directly to the main building blocks, which are

  • entity - an identifiable building block of the application (a module)
  • application - container of entities
  • loader - an entity capable of loading other entities
  • lifecycle_participant - an entity with application managed lifecycle
  • execution context - set of rules for code execution (e.g. a specific thread, a thread pool, a strand on a thread pool, ...)
  • launcher - a binary (ufw_launcher, the entry point into an application)

Configuration

Application configuration language is hierarchical YAML. This format gives a good representation of both application bootstrap process and the runtime structure.

Lifecycle Phases

Application modules are created in the order they are defined in the configuration file and are destroyed in the reverse order. Modules can opt to participate in the structural lifecycle by extending the virtual ufw::lifecycle_participant base. The lifecycle phases lifecycle_participant-s are transitioned through are below. The order of transition among individual participants matches their declaration order in the application configuration file.

  • init() - lifecycle_participants may/should discover and cache strongly typed references to each other and fail fast if anything is missing or is of a wrong type
  • start() - lifecycle_participants may/should establish required connections, spawn threads, etc.
  • up() - lifecycle_participants may start messaging others
  • stop() - opposite of start()
  • fini() - opposite of init()

Loaders

A subset of entities capable of loading other entities is called loaders. A loader entity extends the virtual ufw::loader base. loaders are entities. loaders can load other loaders. A special "seed" loader — the default_loader, is used by the application to load entities by name (including other loaders). Whether or not an entity is loaded with a loader is specified in the config (flexibility!). entities can be registered in the application programmatically without loaders. The application registers the default_loader in directly in the constructor. The default launcher registers LIBRARY (loads shared libaries) and PLUGIN (loads entities from shared libraries) loaders before initiating the application bootstrap.

Concurrency

Application initialisation is done single-threaded in the application main thread. Once the application is up the main thread becomes the host of the default execution context. The default execution context an instance of the boost::asio::io_context accessible from entities via this.context(). All other concurrency models are incremental to the ufw.application.

Logging

Logging is a part of the framework. Modules have scoped tagged loggers (with help of macros and context-sensitive symbol lookup). The Boost.Log library was taken as Boost is already on the dependency list and the subject library is flexible and reliable. This logger is not the fastest around, but it's probably one of the cheapest to integrate with.

The logger is configured the same way as any other entity. The config part of the configuration is passed unchanged to the Boost.Log initializer. See the configuration file fragment below as an example.

Trying It

μFW comes with an example module packaged into a plugin shared library (libexample.so on Linux).

The below configuration fragment has a single instance of the example module.

---
application:

  entities:
    # ====== logger ======
    - name: LOGGER
      config: |
        [Core]
        DisableLogging=false
        LogSeverity=error

        [Sinks.Console]
        Destination=Console
        Format="%TimeStamp(format=\"%H:%M:%S.%f\")% | %Severity(format=\"%6s\")% | %ThreadPID% | %Entity% - %Tag%%Message%"
        Asynchronous=true
        AutoFlush=true

    # ====== a dynamic library ======
    - name: example_lib
      loader_ref: LIBRARY
      config:
        filename: libexample.so

    # ====== an entity -- plugin from a dynamic library ======
    - name: example_plugin
      loader_ref: PLUGIN
      config:
        library_ref: example_lib
        constructor: example_ctor
...

To run it, store the above fragment into a YAML file (say config.yaml) and run the μFW launcher as ufw_launcher -c config.yaml.

The console log should look similar to the below screenshot.

screenshot

Building Dependencies

The author's main development platforms are x86_64 Arch Linux and macOS + Homebrew. Both have quite up to date versions of all μFW dependencies. For those working in a less bleeding-edge enviroronments - below are the instructions for building the dependencies from scratch.

Boost

Use instructions from the Boost Home Page The μFW is using these modules:

  • system
  • program_options
  • log
  • boost_unit_test_framework

YamlCPP

$ git clone https://github.com/jbeder/yaml-cpp.git
$ mkdir yaml-cpp-build && cd yaml-cpp-build
$ cmake -DBUILD_SHARED_LIBS=ON -DCMAKE_BUILD_TYPE=Release ../yaml-cpp
$ make -j$(nproc) && sudo make install

CapNProto (optional - examples)

$ git clone https://github.com/sandstorm-io/capnproto.git
$ mkdir capnproto-build && cd capnproto-build
$ cmake -DCMAKE_BUILD_TYPE=Release ../capnproto/c++
$ make -j$(nproc) && sudo make install

Building

Compiling

$ git clone https://github.com/mrbald/ufw.git
$ mkdir ufw-build && cd ufw-build
$ cmake -DCMAKE_BUILD_TYPE=Release [-DCMAKE_INSTALL_PREFIX=$HOME/local] ../ufw
$ make -j$(nproc)

Running tests

To run all tests run

$ make unit-test

To run individual tests with verbose output run

$ make BOOST_TEST_LOG_LEVEL=all BOOST_TEST_RUN_FILTERS=ufw_app/* unit-test

Running benchmarks

$ make benchmark

Installing

$ sudo make install

Using

TODO

Hacking

TODO

References

CMake/How To Find Libraries

Markdown Cheatsheet

Draw.io