Getting Started

This section gives you a quick tour of the most important concepts in order to get you started with developing bundles and publishing and consuming services.

We will define a service interface, service implementation, and service consumer. A simple executable then shows how to install and start these bundles.

The complete source code for this example can be found at /doc/src/examples/getting_started. It works for both shared and static builds, but only the more commonly used shared build mode is discussed in detail below.

The Build System

These examples come with a complete CMakeLists.txt file showing the main usage scenarios of the provided CMake helper functions. The script requires the CppMicroServices package:

project(CppMicroServicesGettingStarted)

find_package(CppMicroServices REQUIRED)

A Simple Service Interface

Services implement one or more service interfaces. An interface can be any C++ class, but typically contains only pure virtual functions. For our example, we create a separate library containing a service interface that allows us to retrieve the number of elapsed milliseconds since the POSIX epoch:

#include <servicetime_export.h>

#include <chrono>

struct SERVICETIME_EXPORT ServiceTime
{
    virtual ~ServiceTime();

    // Return the number of milliseconds since POSIX epoch time
    virtual std::chrono::milliseconds elapsed() const = 0;
};

The CMake code does not require C++ Micro Services specific additions:

#=========================================================
# A library providing the ServiceTime interface
#---------------------------------------------------------

# Technically, this is not a bundle.

add_library(ServiceTime
  service_time/ServiceTime.h
  service_time/ServiceTime.cpp
  )

target_include_directories(ServiceTime PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/service_time>
  $<BUILD_INTERFACE:${CMAKE_CURRENT_BINARY_DIR}>
)

generate_export_header(ServiceTime)

if(BUILD_SHARED_LIBS)
  set_target_properties(ServiceTime PROPERTIES
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN 1
    )
endif()

Bundle and BundleContext

A bundle is the logical set of C++ Micro Services specific initialization code, metadata stored in a manifest.json resource file, and other resources and code. Multiple bundles can be part of the same or different (shared or static) library or executable. To create the bundle initialization code, you can either use the usFunctionGenerateBundleInit CMake function or the CPPMICROSERVICES_INITIALIZE_BUNDLE macro directly.

In order to publish and consume a service, we need a BundleContext instance, through which a bundle accesses the C++ Micro Services API. Each bundle is associated with a distinct bundle context that is accessible from anywhere in the bundle via the GetBundleContext() function:

#include <cppmicroservices/GetBundleContext.h>

void Dummy()
{
  auto context = cppmicroservices::GetBundleContext();
}

Please note that trying to use GetBundleContext() without proper initialization code in the using library will lead to compile or runtime errors.

Publishing a Service

Publishing a service is done by calling the BundleContext::RegisterService function. The following code for the service_time_systemclock bundle implements the ServiceTime interface as a service:

#include <ServiceTime.h>

#include <cppmicroservices/BundleActivator.h>

using namespace cppmicroservices;

class ServiceTimeSystemClock : public ServiceTime
{
    std::chrono::milliseconds
    elapsed() const
    {
        auto now = std::chrono::system_clock::now();

        // Relies on the de-facto standard of relying on
        // POSIX time in all known implementations so far.
        return std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch());
    }
};

class ServiceTimeActivator : public BundleActivator
{
    void
    Start(BundleContext ctx)
    {
        auto service = std::make_shared<ServiceTimeSystemClock>();
        ctx.RegisterService<ServiceTime>(service);
    }

    void
    Stop(BundleContext)
    {
        // Nothing to do
    }
};

CPPMICROSERVICES_EXPORT_BUNDLE_ACTIVATOR(ServiceTimeActivator)

A std::shared_ptr holding the service object is passed as the an argument to the RegisterService<>() function within a bundle activator. The service is registered as long as it is explicitly unregistered or the bundle is stopped. The bundle activator is optional, but if it is declared, its BundleActivator::Start(BundleContext) and BundleActivator::Stop(BundleContext) functions are called when the bundle is started or stopped, respectively.

The CMake code for creating our bundle looks like this:

#=========================================================
# A bundle implementing the ServiceTime interface
#---------------------------------------------------------

set(_srcs
  service_time_systemclock/ServiceTimeImpl.cpp
  )

# Set up dependencies to resources to track changes
usFunctionGetResourceSource(TARGET ServiceTime_SystemClock OUT _srcs)
# Generate bundle initialization code
usFunctionGenerateBundleInit(TARGET ServiceTime_SystemClock OUT _srcs)

add_library(ServiceTime_SystemClock ${_srcs})

target_link_libraries(ServiceTime_SystemClock CppMicroServices ServiceTime)

set(_bundle_name service_time_systemclock)

set_target_properties(ServiceTime_SystemClock PROPERTIES
  # This is required for every bundle
  COMPILE_DEFINITIONS US_BUNDLE_NAME=${_bundle_name}
  # This is for convenience, used by other CMake functions
  US_BUNDLE_NAME ${_bundle_name}
  )

if(BUILD_SHARED_LIBS)
  set_target_properties(ServiceTime_SystemClock PROPERTIES
    CXX_VISIBILITY_PRESET hidden
    VISIBILITY_INLINES_HIDDEN 1
    )
endif()

# Embed meta-data from a manifest.json file
usFunctionEmbedResources(TARGET ServiceTime_SystemClock
  WORKING_DIRECTORY
    ${CMAKE_CURRENT_SOURCE_DIR}/service_time_systemclock
  FILES
    manifest.json
  )

In addition to the generated bundle initialization code, we need to specify a unique bundle name by using the US_BUNDLE_NAME compile definition as shown above.

We also need to provide the manifest.json file, which is added as a resource and contains the following JSON data:

{
  "bundle.symbolic_name" : "service_time_systemclock",
  "bundle.activator" : true,
  "bundle.name" : "Service Time SystemClock",
  "bundle.description" : "This bundle uses std::chrono::system_clock"
}

Because our bundle provides an activator, we also need to state its existence by setting the bundle.activator key to true. The last two elements are purely informational and not used directly.

Consuming a Service

The process to consume a service is very similar to the process for publishing a service, except that consumers need to handle some additional error cases.

Again, we use a bundle activator to execute code on bundle start that retrieves and consumes a ServiceTime service:

#include <cppmicroservices/BundleActivator.h>
#include <cppmicroservices/BundleContext.h>
#include <cppmicroservices/GetBundleContext.h>

#include <ServiceTime.h>

#include <iostream>

using namespace cppmicroservices;

class ServiceTimeConsumerActivator : public BundleActivator
{
    typedef ServiceReference<ServiceTime> ServiceTimeRef;

    void
    Start(BundleContext ctx)
    {
        auto ref = ctx.GetServiceReference<ServiceTime>();

        PrintTime(ref);
    }

    void
    Stop(BundleContext)
    {
        // Nothing to do
    }

    void
    PrintTime(ServiceTimeRef const& ref) const
    {
        if (!ref)
        {
            std::cout << "ServiceTime reference invalid" << std::endl;
            return;
        }

        // We can also get the bundle context like this
        auto ctx = GetBundleContext();

        // Get the ServiceTime service
        auto svc_time = ctx.GetService(ref);
        if (!svc_time)
        {
            std::cout << "ServiceTime not available" << std::endl;
        }
        else
        {
            std::cout << "Elapsed: " << svc_time->elapsed().count() << "ms" << std::endl;
        }
    }
};

CPPMICROSERVICES_EXPORT_BUNDLE_ACTIVATOR(ServiceTimeConsumerActivator)

Because the C++ Micro Services is a dynamic environment, a particular service might not be available yet. Therefore, we first need to check the validity of some returned objects.

The above code would be sufficient only in the simplest use cases. To avoid bundle start ordering problems (e.g. one bundle assuming the existence of a service published by another bundle), a ServiceTracker should be used instead. Such a tracker allows bundles to react on service events and in turn be more robust.

The CMake code for creating a library containing the bundle is very similar to the code for the publishing bundle and thus not included here.

Installing and Starting Bundles

The two bundles above are embedded in separate libraries and need to be installed into a Framework and started. This is done by a small example program:

#include <cppmicroservices/Bundle.h>
#include <cppmicroservices/BundleContext.h>
#include <cppmicroservices/BundleImport.h>
#include <cppmicroservices/Framework.h>
#include <cppmicroservices/FrameworkFactory.h>

using namespace cppmicroservices;

int
main(int argc, char* argv[])
{
#ifdef US_BUILD_SHARED_LIBS
    if (argc < 2)
    {
        std::cout << "Pass shared libraries as command line arguments" << std::endl;
    }
#endif

    // Create a new framework with a default configuration.
    Framework fw = FrameworkFactory().NewFramework();

    try
    {
        // Initialize the framework, such that we can call
        // GetBundleContext() later.
        fw.Init();
    }
    catch (std::exception const& e)
    {
        std::cout << e.what() << std::endl;
        return 1;
    }

    // The framework inherits from the Bundle class; it is
    // itself a bundle.
    auto ctx = fw.GetBundleContext();
    if (!ctx)
    {
        std::cerr << "Invalid framework context" << std::endl;
        return 1;
    }

    // Install all bundles contained in the shared libraries
    // given as command line arguments.
    for (int i = 1; i < argc; ++i)
    {
        try
        {
            ctx.InstallBundles(argv[i]);
        }
        catch (std::exception const& e)
        {
            std::cerr << e.what() << std::endl;
        }
    }

    try
    {
        // Start the framwork itself.
        fw.Start();

        // Our bundles depend on each other in the sense that the consumer
        // bundle expects a ServiceTime service in its activator Start()
        // function. This is done here for simplicity, but is actually
        // bad practice.
        auto bundles = ctx.GetBundles();
        auto iter = std::find_if(bundles.begin(),
                                 bundles.end(),
                                 [](Bundle& b) { return b.GetSymbolicName() == "service_time_systemclock"; });
        if (iter != bundles.end())
        {
            iter->Start();
        }

        // Now start all bundles.
        for (auto& bundle : bundles)
        {
            bundle.Start();
        }
    }
    catch (std::exception const& e)
    {
        std::cerr << e.what() << std::endl;
    }

    return 0;
}

#if !defined(US_BUILD_SHARED_LIBS)
CPPMICROSERVICES_IMPORT_BUNDLE(service_time_systemclock)
CPPMICROSERVICES_IMPORT_BUNDLE(service_time_consumer)
#endif

The program expects a list of file system paths pointing to installable libraries. It will first construct a new Framework instance and then install the given libraries. Next, it will start all available bundles.

When the Framework instance is destroyed, it will automatically shut itself down, essentially stopping all active bundles.

See also

A more detailed tutorial demonstrating some more advanced features is also available.