Improbable Icon

Example C++ Worker

v10-1-0

#1

Hi

Are there any examples of custom C++ workers that work on SpatialOS 10? All I can find is an old tutorial for SpatialOS 7 at https://spatialos.improbable.io/docs/reference/8.0/workers/cpp/tutorial.

I found this: https://github.com/jacqueselliott/cpp-working-files, which helped to get the worker compiling, but I’m wondering if there’s a more complete example?


#2

Hello @nferguson,

Unfortunately we do not have an example in our documentation at the moment as you have noticed and our tutorials are focused around Unity workers. So there is no complete worker example available. I will try and post a kind-of skeleton code here tomorrow that should get you going. In the meanwhile you can have a look at the another file by the same author that you already linked: https://github.com/jacqueselliott/SpatialRL/blob/master/workers/rlworker/rlworker.cc

Duco


#3

Thanks. I eventually got a C++ worker building on Mac and Linux (haven’t tried Windows). If anyone’s interested here’s my CMakeLists.txt:

cmake_minimum_required (VERSION 3.0)
project (myworker)

set(CMAKE_VERBOSE_MAKEFILE ON)

set(CMAKE_CXX_STANDARD 11)

if(${CMAKE_SYSTEM_NAME} STREQUAL "Darwin")
    set(SDK_PATH worker_sdk_mac)
elseif(${CMAKE_SYSTEM_NAME} STREQUAL "Windows")
    set(SDK_PATH worker_sdk_windows)
else()
    set(SDK_PATH worker_sdk_linux)
endif()

if(MSVC)
    # Static runtime on windows to avoid requiring the MSVCRT*.dll.
    foreach(FLAG_VAR
            CMAKE_CXX_FLAGS CMAKE_CXX_FLAGS_DEBUG CMAKE_CXX_FLAGS_RELEASE
            CMAKE_CXX_FLAGS_MINSIZEREL CMAKE_CXX_FLAGS_RELWITHDEBINFO)
        if(${FLAG_VAR} MATCHES "/MD")
            string(REGEX REPLACE "/MD" "/MT" ${FLAG_VAR} "${${FLAG_VAR}}")
        endif()
    endforeach()
elseif(NOT APPLE)
    # Static GCC C++ and C standard libraries.
    set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -static-libstdc++ -static-libgcc")

    # 32/64-bit switch on linux. Should be before adding the external subdirectory, since their
    # setting must match.
    if(LINUX_32BIT)
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m32")
    endif()
    if(LINUX_64BIT)
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -m64")
    endif()
endif()

if(NOT MSVC)
    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pthread")
endif()

include_directories("${PROJECT_SOURCE_DIR}/${SDK_PATH}/include")
include_directories("${PROJECT_SOURCE_DIR}/generated")

find_package(PythonLibs)
include_directories(${PYTHON_INCLUDE_DIRS})

find_library(LIB_RAKNET RakNetLibStatic "${PROJECT_SOURCE_DIR}/${SDK_PATH}/lib" NO_DEFAULT_PATH)
find_library(LIB_Z_STATIC z "${PROJECT_SOURCE_DIR}/${SDK_PATH}/lib" NO_DEFAULT_PATH)
find_library(LIB_SSL ssl "${PROJECT_SOURCE_DIR}/${SDK_PATH}/lib" NO_DEFAULT_PATH)
find_library(LIB_PROTO NAMES protobuf libprotobuf PATHS "${PROJECT_SOURCE_DIR}/${SDK_PATH}/lib" NO_DEFAULT_PATH)
find_library(LIB_GPR gpr "${PROJECT_SOURCE_DIR}/${SDK_PATH}/lib" NO_DEFAULT_PATH)
find_library(LIB_GPRC grpc "${PROJECT_SOURCE_DIR}/${SDK_PATH}/lib" NO_DEFAULT_PATH)
find_library(LIB_GPRCPP grpc++ "${PROJECT_SOURCE_DIR}/${SDK_PATH}/lib" NO_DEFAULT_PATH)
find_library(LIB_WORKER_SDK WorkerSdk "${PROJECT_SOURCE_DIR}/${SDK_PATH}/lib" NO_DEFAULT_PATH)
find_library(LIB_CORE_SDK CoreSdk "${PROJECT_SOURCE_DIR}/${SDK_PATH}/lib" NO_DEFAULT_PATH)

FILE(GLOB_RECURSE SOURCES "${PROJECT_SOURCE_DIR}/*.cc")

# Build the worker
add_executable(myworker ${SOURCES})
target_link_libraries(myworker
        ${PYTHON_LIBRARIES}
        ${PROJECT_SOURCE_DIR}/${LIB_WORKER_SDK}
        ${PROJECT_SOURCE_DIR}/${LIB_CORE_SDK}
        ${PROJECT_SOURCE_DIR}/${LIB_GPRCPP}
        ${PROJECT_SOURCE_DIR}/${LIB_GPRC}
        ${PROJECT_SOURCE_DIR}/${LIB_GPR}
        ${PROJECT_SOURCE_DIR}/${LIB_PROTO}
        ${PROJECT_SOURCE_DIR}/${LIB_SSL}
        ${PROJECT_SOURCE_DIR}/${LIB_Z_STATIC}
        ${PROJECT_SOURCE_DIR}/${LIB_RAKNET}
        ${CMAKE_DL_LIBS}
        )

You can leave the Python stuff out – that’s not essential.


#4

Nice work! I’m still doing the C++ worker skeleton, have been held back by other things a little bit.

One important thing to note is, while it is quite practical to use CMake, you can use any kind-of build system you want (CMake, Makefiles, Automake, Bazel, …). The only requirement for you is to invoke the right build steps in your custom build configuration.

Duco


#5

As promised I’m putting here some sort of skeleton for a C++ worker. It includes most things that you would need to get to the point where the worker is connected to the deployment. There is no “core-logic” as that would be specific to your own worker. It should be noted that this is can also be used as an abstract starting point for a C# or Java worker as the logic would be nearly identical.

// Copyright (c) Improbable Worlds Ltd, All Rights Reserved
#include <improbable/standard_library.h>
#include <improbable/witness.h>
#include <improbable/worker.h>
#include <chrono>
#include <cstdint>
#include <cstdlib>
#include <iostream>
#include <string>
#include <thread>


using namespace improbable;

int main(int argc, char** argv) {
  // The all-important help message that explains the expected positional arguments.
  auto print_usage = [&]() {
    std::cout << "Usage: " << argv[0] << " client <deployment_id> <login_token>" << std::endl;
    std::cout << "Usage: " << argv[0] << " server <hostname> <port> <worker_id>" << std::endl;
    std::cout << std::endl;
    std::cout << "Connects to the deployment reachable at <hostname>:<post> as server worker";
    std::cout << std::endl;
    std::cout << "or as a client to the cloud deployment with the given ID and login token.";
    std::cout << std::endl;
    std::cout << std::endl;
    std::cout << "Example: " << argv[0] << " server localhost 7777 CppClient0" << std::endl;
  };

  if (!((argc == 4 && std::string(argv[1]).compare("client") == 0) ||
        (argc == 5 && std::string(argv[1]).compare("server") == 0))) {
    print_usage();
    return 1;
  }

  // Some usefull constants that may be used throughout the worker's main function.
  static const std::string projectName = "myProject";
  static const std::string workerType = "CppClient";
  static const std::string workerLogName = "cpp_client";

  static const int workerConnectionMultiplexLevel = 4;
  static const worker::NetworkConnectionType workerConnectionProtocol =
    worker::NetworkConnectionType::kTcp;

  // Parse the arguments and initialise the connection parameters.
  bool isClient = false;
  if (std::string(argv[1]).compare("client") == 0) {
    isClient = true;
  }

  worker::ConnectionParameters connection_parameters;
  connection_parameters.WorkerType = workerType;
  connection_parameters.Network.ConnectionType = workerConnectionProtocol;
  connection_parameters.Network.Tcp.MultiplexLevel = workerConnectionMultiplexLevel;
  connection_parameters.NetworkConnectionType.UseExternalIp = isClient;

  // Connect the worker to the deployment.
  auto connect = [&argv, &connection_parameters, isClient]() {
    if (isClient) {
      const std::string deployment_id = argv[2];
      const std::string login_token = argv[3];

      // Set up the Locator object which we will use to connect to the deployment as a client.
          worker::LocatorParameters locator_parameters;
          locator_parameters.ProjectName = projectName;
          locator_parameters.CredentialsType = worker::LocatorCredentialsType::kLoginToken;
          locator_parameters.LoginToken = login_token;

          worker::Locator locator{"locator.improbable.io", locator_parameters};

          // Create a lambda-function as callback to manage queueing.
          auto queue_status_callback [&](const worker::QueueStatus& queue_status) {
            if (!queue_status.Error.empty()) {
              std::cerr << "An error occured while queueing: " << *queue_status.Error << std::endl;
              return false;
            }
            std::cout << "Queueing... Current position: " << queue_status.PositionInQueue << std::endl;
            return true;
          };

          std::cout << "Connecting through locator." << std::endl;
          auto future =
            locator.ConnectAsync(deployment_id, connection_parameters, queue_status_callback);
          return future.Get();
        } else {
          const std::string hostname = argv[2];
          const std::uint16_t port = static_cast<std::uint16_t>(std::stoi(argv[3]));
          const std::string worker_id = argv[4];

          auto future =
            worker::Connection::ConnectAsync(hostname, port, worker_id, connection_parameters);
          return future.Get();
        }
      }
  worker::Connection connection = connect();

  // Give the default callbacks for connection events, metrics and logging.
  view.OnDisconnect([&](const worker::DisconnectOp& op) {
    std::cerr << "[disconnected] " << op.Reason << std::endl;
    disconnect = true;
  });

  view.OnMetrics([&](const worker::MetricsOp& op) {
    auto metrics = op.Metrics;
    connection->SendMetrics(metrics);
  });

  view.OnLogMessage([](const worker::LogMessageOp& message) {
    switch (message.Level) {
    case worker::LogLevel::kDebug:
      std::cout << "[debug] " << message.Message << std::endl;
      break;
    case worker::LogLevel::kInfo:
      std::cout << "[info] " << message.Message << std::endl;
      break;
    case worker::LogLevel::kWarn:
      std::cerr << "[warning] " << message.Message << std::endl;
      break;
    case worker::LogLevel::kError:
      std::cerr << "[error] " << message.Message << std::endl;
      break;
    case worker::LogLevel::kFatal:
      std::cerr << "[fatal] " << message.Message << std::endl;
      std::terminate();
    default:
      break;
    }
  });

  // Verify that the connection was actually successful for a maximum of 10 seconds.
  int verificationAttempts = 0;
  while (!connection->IsConnected()) {
    verificationAttempts++;
    if (verificationAttempts >= 10) {
      std::cerr << "Failed to establish a connection with the deployment." << std::endl;
      view.Process(connection->GetOpList(0));
      return EXIT_FAILURE;
    }
    std::this_thread::sleep_for(std::chrono::seconds(1));
  }

  // Core logic of the worker.
  // [...]

  return EXIT_SUCCESS
}