Project Description

Vex is a collection of lightweight, extensible and easy to use C++ components intended to solve a couple of vexing problems inherent to ANSI C++. Specifically, it provides a framework for writing binary safe components with plain ANSI C++. With Vex you can, for example, easily take a range from std::vector, pass it across boundaries of your binary component (a dynamic linked library or shared object) and use it safely on the "other side". In the same way you can define and use single- and multicast-delegates, events, Linq-like sequences of filters and much more.

Rationale

Vex is intended to provide a universal framework to simplify creation of binary compatible components in pure ANSI C++. In this context, binary compatibility means: The binary component created for one platform is compatible with other components created for that platform regardless of the toolset or configuration of the toolset used to created the components. For example: A component created for Windows (x86) platform using the toolset vc110 in "Release" configuration is compatible with all components created for Windows x86 regardless of the toolset or the toolset configuration. In this context a binary component is a library or program which provides certain services advertised through a set of contracts and promises to be binary compatible or put simply - it provides a public Application Binary Interface (ABI).

Vex defines a set of contracts, instances of which can be safely exchanged across binary component boundaries. A contract is defined as something that almost all platforms agree on as being binary compatible across different tool-sets or tool-set settings: a pure abstract class with custom memory management facilities.

Background

The template-based C++ Standard Library was designed for maximum efficiency and flexibility. Boost and other popular C++ libraries followed the paradigm. The consequence of the design is that one aspect falls short: communication between binary safe components via data structures defined by such libraries. The ability to exchange instances of types or invoke functions which take or return objects across component boundaries is limited to libraries that use compatible (or identical) compiler and linker settings. In many scenarios you are forced to write pure generic code and deliver it as such, where you'd rather hide the implementation. Think of:
  • abstracting away the kind of used container or iterator
  • forward a complex range created by filtering sequence of Boost.Range or
  • expose a Boost.Signal in a public API etc.

Design: Abstract contracts, type erasure and handle-body idiom

The Standard library already offers some type erasing constructs, for example std::function. Boost libraries have any_range or any. Vex basically follows the same idiom, sometimes referred to as handle-body pattern or wrapper pattern. However, Vex also defines a public abstract contract for the body part, for example:
  • Vex.Functional defines a vex::any_delegate<void ()> which is an equivalent of std::function<void ()>.
  • any_delegate<void ()> is a handle for abstract_delegate<void ()>*.
  • You can create either any_delegate<void ()> or a instance of abstract_delegate<void ()>* from any instance of a callable type compatible with given signature or from any other instance of abstract_delegate<void ()>*.
  • A pointer to an instance of any Vex contract can be pulled out of its wrapper and safely passed across binary boundaries of your component as long as few conventions are kept.
  • Conceptually, any Vex handle acts as a smart pointer with publically accessible body of the proper contract type.
  • By convention, all Vex wrapper-types (handles) use the prefix any_ and all Vex contract-types use the prefix abstract_.

Project status

Vex is a work-in-progress project. For current state and associated issues, please refer to vex.devkit. Currently, following libraries are scheduled for the first release:
  • Vex.Core: Common components and utilities
  • Vex.Iterable: Expose C++ Standard Library conforming ranges and containers as binary safe contracts
  • Vex.Functional: Exposes callable types (signals, delegates and events) as binary safe contracts
  • Vex.Query: Implements Linq-like queries and manipulators for types conforming to Vex.Iterable concepts

Any kind of cooperation, feedback or criticism are welcome...

Project structure

Each Vex library is embedded in a namespace vex::[library name], e.g. vex::iterable. The library names correspond to the folders inside the top level vex folder. Inside the library, there are typically two other namespaces:
  1. contract: set of contracts used to represent specific functionality
  2. implementation: Default implementations which conform with the contract specifications.

Most libraries contain further "standardized" units which are given names using the following conventions:
  1. A set of factory functions called make_[kind of the result type]_[kind of the element]. These functions create instances of concrete types which implement a (set of) contract(s) using the types from implementation namespace. For example, make_raw_range creates a raw pointer to a default implementation of contract::range based on the passed parameters. Similarly, make_intrusive_range creates a boost::intrusive_ptr<range> and so on.
  2. A set of smart pointer-like wrappers for instances of various contracts, e.g. a wrapper for any contract::range contract is called any_range etc. There are also factory functions to create instances of any_ wrappers called make_any_, for example: make_any_range, make_any_delegate etc.

In order to build and test the library (which is currently header only) you'll need to check out the vex SDK which contains build scripts, dependencies and tests.

Motivating Example

Vex.Iterable library defines contracts for various types of iterable ranges and sequences and a set of factory functions to create default implementations from instances of types conforming to C++ Standard Library concepts of a Range or Container. The Vex.Iterable types are not conforming to C++ Standard library container or range concepts - it is terribly awkward to create an abstract model for STL iterator pairs and containers. Instead Vex.Iterable adopts (and slightly modifies) the concept of Ranges from D-language Standard Library std.range which are more appropriate for this purpose.

Assume, there is a component foo with binary safe API which needs to consume an iterable range of values. Because of the binary safety requirement, it is not acceptable to pass C++ Standard Library containers or iterator ranges directly. With Vex, the component declares an API which uses a binary safe contract defined by Vex:
// foo/api.h
#include <vex/iterable/contract/range.h>
    
namespace foo { 
    void api(vex::abstract_range<vex::random_access_tag, int&>* p_range);
}

The client uses C++ Standard Library or Boost data types as usual. When it needs to pass an instance of a Standard Library container or a conforming Boost type to foo it creates a default implementation of the contract using a factory function which creates a wrapper element, in this example a type erasing RAII safe wrapper with standard smart pointer semantics for ranges:
#include <vex/iterable/make_any_range.h>
#include <foo/api.h>

int main() 
{
    namespace vi = vex::iterable;

    std::vector<int> t_source = {
        0, 1, 2, 3, 4, 5, 6
    };
    
    // Create a std::vector wrapper and pass an instance to foo_component::api. 
    // 'auto' resolves to vex::iterable::any_range<vex::iterable::random_access_tag, int&>:
    if (auto r = vex::make_any_range(t_source)) {
        // Pass instance of raw pointer to an embedded contract to foo: 
        foo::api(r.get());
    }
}

On foo's side, there is nothing known about the implementation details, or the used Standard Library container, you only include a Vex contract declaration:
// pulls in the contract declaration
#include <vex/iterable/contract/range.h>
// pulls in the STL interoperability layer (depends on Boost.Iterator and Boost.Range)
#include <vex/iterable/stl_interop.h>

namespace foo { 
    // uses "native" vex range API
    void api_impl_version_1(vex::abstract_range<vex::random_access_tag, int&>* p_range)
    {
        // We're not taking ownership of p_range, so no need to manipulate reference counters...
        if (p_range) {
            while (p_range->move_front()) {
                p_range->front() += 1;
            }
        }
    }
    
    // uses STL interop API:
    void api_impl_version_2(vex::abstract_range<vex::random_access_tag, int&>* p_range)
    {
        // Use STL compatibility API: Here a STL compatible adapter is 
        // created in order to use a range based for loop:
        for (auto & v : vex::iterable::make_iterator_range(p_range)) {
            v += 1;
        }
    }

    void api(vex::abstract_range<vex::random_access_tag, int&>* p_range)
    {
        // use whichever version you like...
        api_impl_version2(p_range);
    }
}

Last edited Aug 10 at 1:38 PM by michalik, version 63