μ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 →
- 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.
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)
Application configuration language is hierarchical YAML. This format gives a good representation of both application bootstrap process and the runtime structure.
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 typestart()
- lifecycle_participants may/should establish required connections, spawn threads, etc.up()
- lifecycle_participants may start messaging othersstop()
- opposite ofstart()
fini()
- opposite ofinit()
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.
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 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.
μ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.
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.
Use instructions from the Boost Home Page The μFW is using these modules:
- system
- program_options
- log
- boost_unit_test_framework
$ 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
$ 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
$ 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)
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
$ make benchmark
$ sudo make install
TODO
TODO