C++ Microservices in Docker: Adding IMSL C Numerical Library
April 22, 2021

C++ Microservices in Docker: Adding a Numerical Library

Embedded Analytics

There are a variety of frameworks and tools available for implementing a microservice architecture, however, it isn’t always clear how to expose native code like C or C++ code within a wider microservice system. In this series, we have covered the basics of a C++ Microservices deployment including: 

With those basics in place, this article will look at deploying a new C++ servlet that relies on the IMSL C Numerical Library (CNL), demonstrating how 3rd party libraries can be incorporated into a Docker application to support writing more advanced servlets.

Create a Skeleton for the New Service

For this example we’ll be leveraging CNL as an example 3rd party library, however the same basic approach should be applicable to any 3rd party library you may want to rely on.

We’ll be building on the Dockerfile that was assembled at the end of the Docker optimization article. (Go grab it now and follow along!)

We’ll start by adding a skeleton for our new service to the build infrastructure and the Dockerfile. Similar to the “HelloWorldServlet” we’re currently deploying, we’ll create a new servlet context “random”. Within that we’ll create a new servlet “UniformServlet” that will return a uniform distribution of random numbers and use our existing “hello” context, servlet, and build infrastructure as a reference to create the new context. Those files (with the necessary changes from their “hello” counterparts highlighted) are outlined below:

C++ Microservices in Docker: Adding IMSL C Numerical Library

src/random/UniformServlet.cpp

#include <string>

#include <rwsf/servlet/ServletOutputStream.h>
#include <rwsf/servlet/http/HttpServlet.h>
#include <rwsf/servlet/http/HttpServletRequest.h>
#include <rwsf/servlet/http/HttpServletResponse.h>

class UniformServlet : public rwsf::HttpServlet {
public:
    void doGet(rwsf::HttpServletRequest& request, rwsf::HttpServletResponse& response) {
        response.setContentType("text/html");
        rwsf::ServletOutputStream& out = response.getOutputStream();

        out.println("<html><body><h1>Uniform!</h1></body></html>");
    }
};

RWSF_DEFINE_SERVLET(UniformServlet)

We’ll revisit the implementation of this servlet once we’ve deployed CNL, but for now we’ll just return a simple HTML message.

src/random/WEB-INF/web.xml

<web-app>
    <servlet>
        <servlet-name>Uniform</servlet-name>
        <servlet-class>random.createUniformServlet</servlet-class>
    </servlet>
    <servlet-mapping>
        <servlet-name>Uniform</servlet-name>
        <url-pattern>/uniform</url-pattern>
    </servlet-mapping>
</web-app>

src/random/CMakeLists.txt

cmake_minimum_required(VERSION 3.9)

project(random VERSION 1.0 LANGUAGES CXX)

add_library(random SHARED UniformServlet.cpp)
target_compile_features(random PRIVATE cxx_auto_type)
target_link_libraries(random RWSF::Servlet)

src/CMakeLists.txt

cmake_minimum_required(VERSION 3.9)

project(servlets VERSION 1.0 LANGUAGES CXX)

add_library(RWSF::Servlet SHARED IMPORTED)
set_target_properties(RWSF::Servlet PROPERTIES
    IMPORTED_LOCATION $ENV{RWSF_HOME}/lib/librwsf_servlet20012d.so)
target_compile_definitions(RWSF::Servlet INTERFACE -D_RWCONFIG=12d)
target_include_directories(RWSF::Servlet INTERFACE $ENV{RWSF_HOME}/include)

add_subdirectory(hello)
add_subdirectory(random)

We’ll also need to update our Dockerfile to deploy the new context to our container:

Dockerfile

…
COPY --from=servlet_build /src/hello/WEB-INF ${RWSF_HOME}/apps/servlets/hello/WEB-INF/

COPY --from=servlet_build /build/random/librandom.so ${RWSF_HOME}/apps-lib/
COPY --from=servlet_build /src/random/WEB-INF ${RWSF_HOME}/apps/servlets/random/WEB-INF/

COPY entrypoint.sh /entrypoint.sh
…

Add the Numerical Library to a Container

Now that we have skeleton for our new servlet in place, we can move on to incorporating IMSL C into our container.

First, we need to download and install IMSL C, just as we did for HydraExpress. The process for the two is very similar, and they have similar requirements. We’ll start by creating a base image to avoid duplicating work between the two installations:

 

Dockerfile

FROM centos:7 AS install

RUN yum install -y wget

RUN mkdir -p /opt/download

FROM install AS hydraexpress_install
…

With our base image in place, we can replicate hydraexpress_install to install IMSL C. 

For this example we’ll deploy the IMSL C 2019 Evaluation release of the product. Similar to HydraExpress, deploying a different version or different variant of the product will require slightly different options when invoking the installer, but once installed the behavior and process should be the same. Since we’ll be using an evaluation version of IMSL C, you’ll need an evaluation license. If you’ve already requested an evaluation, the imsl_eval.dat file should be attached to your confirmation email.(If you need a license to try this contact us.)

…
RUN /opt/download/hydraexpress.run  \
    --mode unattended  \
    --prefix /opt/perforce/hydraexpress  \
    --license-file /opt/download/license.key

FROM install as cnl_install

RUN wget -q -O /opt/download/cnl.run \
    https://dslwuu69twiif.cloudfront.net/imsl/cnl/2019/cnl-2019.0.0-lnxgc485x64_eval.run

RUN chmod a+x /opt/download/cnl.run

RUN /opt/download/cnl.run  \
    --mode unattended  \
    --prefix /opt/perforce

COPY imsl_eval.dat /opt/perforce/license/imsl_eval.dat

FROM centos:7 AS servlet_build
…

Update Servlet to Provide Random Numbers Function

With IMSL C now available, we can update our build script to incorporate IMSL C into our servlet to provide the function generating our random numbers. We’ll update our Uniform servlet so that it accepts a parameter, the number of random numbers it should generate, and returns that number of numbers. We’ll use IMSL’s imsl_d_random_uniform function to generate the random numbers that will be returned:

src/random/UniformServlet.cpp

…
#include <rwsf/servlet/http/HttpServletResponse.h>

#include <imsl.h>

class UniformServlet : public rwsf::HttpServlet {
public:
    void doGet(rwsf::HttpServletRequest& request, rwsf::HttpServletResponse& response) {
        auto count = std::stoi(request.getQueryString());

        typedef std::unique_ptr<double[], void(*)(void*)> double_array;

        double_array values(imsl_d_random_uniform(count, 0), imsl_free);

        response.setContentType("text/plain");
        auto& out = response.getOutputStream();
        for (size_t i = 0; i < count; ++i) {
            out.println(values[i]);
        }
    }
};
…

We’ll also need to update our CMake configuration to include the IMSL C libraries. We’ll start with the root CMakeLists.txt to introduce the new libraries:

src/CMakeLists.txt

…
target_include_directories(RWSF::Servlet INTERFACE $ENV{RWSF_HOME}/include)

find_package(OpenMP)

add_library(IMSL::CMath SHARED IMPORTED)
set_target_properties(IMSL::CMath PROPERTIES
    IMPORTED_LOCATION $ENV{CNL_DIR}/$ENV{LIB_ARCH}/lib/libimslcmath_imsl.so)
target_include_directories(IMSL::CMath INTERFACE $ENV{CNL_DIR}/$ENV{LIB_ARCH}/include)
target_link_libraries(IMSL::CMath INTERFACE OpenMP::OpenMP_CXX)

add_subdirectory(hello)
…

Next, we need to leverage the new imported library when building the random servlet context library:

src/random/CMakeLists.txt

…
target_compile_features(random PRIVATE cxx_auto_type)
target_link_libraries(random RWSF::Servlet IMSL::CMath)

With our changes to our servlet complete, we can focus on updating the build step in our Dockerfile. Since we’re now depending on IMSL C, we’ll need to deploy it and configure the environment so that our Servlet can link against its libraries:

Dockerfile

…
ENV RWSF_HOME /opt/perforce/hydraexpress 
ENV CNL_DIR /opt/perforce/imsl/cnl-2019.0.0
ENV LIB_ARCH lnxgc485x64

COPY --from=hydraexpress_install ${RWSF_HOME} ${RWSF_HOME}
COPY --from=cnl_install ${CNL_DIR}/${LIB_ARCH} ${CNL_DIR}/${LIB_ARCH}

RUN yum install -y epel-release
…

Deploy the Final Image

Finally, we can look towards deploying our new dependency to the final image. We need to copy the IMSL library that we depend on along with the evaluation license file to our final image.

We also need to set the IMSL_LIC_FILE to allow IMSL C to locate the license file at runtime. Since our final image is based on a slim Alpine Linux image, we need to install an additional package, libgomp. We also need to add /lib64 to the LD_LIBRARY_PATH environment variable to allow dependencies to be found by the IMSL library.

Dockerfile

…
FROM alpine:latest

RUN apk update --no-cache && apk upgrade --no-cache && apk add --no-cache bash libstdc++ libc6-compat libgomp
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2

ENV RWSF_HOME /opt/perforce/hydraexpress
ENV IMSL_LIC_FILE /opt/perforce/license/imsl_eval.dat
ENV LD_LIBRARY_PATH /lib64:$LD_LIBRARY_PATH

COPY --from=hydraexpress_deploy ${RWSF_HOME} ${RWSF_HOME}

COPY --from=servlet_build /build/hello/libhello.so ${RWSF_HOME}/apps-lib/
COPY --from=servlet_build /src/hello/WEB-INF ${RWSF_HOME}/apps/servlets/hello/WEB-INF/

COPY --from=servlet_build /build/random/librandom.so ${RWSF_HOME}/apps-lib/
COPY --from=servlet_build /src/random/WEB-INF ${RWSF_HOME}/apps/servlets/random/WEB-INF/
COPY --from=cnl_install /opt/perforce/imsl/cnl-2019.0.0/lnxgc485x64/lib/libimslcmath_imsl.so  \
                        /opt/perforce/imsl/cnl-2019.0.0/lnxgc485x64/lib/libimslcmath_imsl.so
COPY --from=cnl_install /opt/perforce/license /opt/perforce/license

COPY entrypoint.sh /entrypoint.sh

With the Dockerfile updated, we’re ready to rebuild and redeploy our C++ microservice:

$ docker build -t hydraexpress .

…

$ docker run --rm -it -p 8090:8090 hydraexpress
*******************************************************************************
 RWSF (TM) - Server Control Script
 Copyright (c) 2001-2020 Rogue Wave Software, Inc., a Perforce company.
 All Rights Reserved.
*******************************************************************************
  RWSF_HOME = /opt/perforce/hydraexpress
  RWSP_HOME = /opt/perforce/hydraexpress/3rdparty/sourcepro
  Starting Rogue Wave Agent...
   INFO| Loading context: /hello/
   INFO| Loading context: /random/
   INFO| Locale directory set to [/opt/perforce/hydraexpress/conf/locale]
   INFO| Default locale set to [en_US]
   INFO| Loading locale [en_US], catalog [messages_en_US.xml]
   INFO| Starting 'HTTP/1.1' connector...

Finally, we can use curl test our new random number generation, requesting five random numbers:

$ curl http://localhost:8090/random/uniform?5
0.816719443451948
0.603686096893477
0.15223048867296
0.537823126436129
0.193286012016836

Providing Numeric Functionality in C++ Microservice

We’ve successfully deployed a C++ microservice that leverages IMSL C to provide numeric functionality. You can apply the same patterns to deploy other 3rd party libraries that may be necessary to fully implement our microservice and make it accessible to the other services in our infrastructure.

Interested in trying this for yourself, request an evaluation copy of HydraExpress and IMSL today. 

Try IMSL   Try HydraExpress

Read Our Full Blog Series on C++ Microservices in Docker: