SoftBank Robotics documentation What's new in NAOqi 2.8?

Making Services using QiLang

QiLang helps writing Qi Services. Its purpose is to define the interface of the service, written in the QiLang Interface Definition Language (IDL). Based on this interface, QiLang generates automatically:

  • the code for strongly-typed proxies for the client code
  • GMock classes for testing.

Strongly-typed Proxies

Strongly-typed proxies improve the client code by allowing numerous mistakes to be detected at buildtime, instead of at runtime. For example:

auto ret = myservice.call<void>("workIt", 42, "blah");

will compile but will fail at runtime because the function expects first a string argument and then an integer. Using QiLang allows you to write:

auto ret = myservice->workIt(42, "blah");

and get a compile-time error because of the invalid arguments.

The code generated by qilang can then be put in a library so that it can be exposed to clients who may compile against it.

If you need details about the qilang syntax, see qilang IDL syntax.

Creating your Service Project

It is advised to use a template to create your project using a simple line of command, from a QiBuild worktree:

$ cd <worktree>
$ qisrc create -i <qilang_repository>/templates/qi-service-cpp MyService

Note

The case used for naming the project is kept only for the name of the service in the service directory, and the name of the classes in the C++ code.

It should produce the following layout of files:

myservice
│
├── myservice
│   ├── CMakeLists.txt
│   ├── qi
│   │   └── myservice
│   │       ├── api.hpp
│   │       └── myservice.idl.qi
│   └── qiproject.xml
│
├── myservice_impl
│   ├── CMakeLists.txt
│   ├── qiproject.xml
│   ├── src
│   │   ├── myservice_impl.hpp
│   │   └── myservice_module.cpp
│   └── test
│       └── test_myservice.cpp
│
├── qiproject.xml
└── README.rst

This layout consists in two QiBuild Projects:

  • myservice exposes the API defined in qi/myservice/myservice.idl.qi, and will generate automatically various C++ files at buildtime, according to what the *.idl.qi files provide. Clients would depend on this project to use the strongly-typed proxies for your service.
  • myservice_impl is where to put the implementation and the tests for your service. Clients do not need to know about this project to use your service.

To start working quick on your project, remember that:

  • myservice/qi/myservice/myservice.idl.qi is where your service’s API is defined, using the QiLang IDL.
  • myservice_impl/src/myservice_impl.hpp is where your implementation lies. It must match the service’s API.
  • myservice_impl/test/test_myservice.cpp is where to put your automatic tests, to quickly check the sanity of your work.

Building

  1. Only the first time: make sure you have set your host config, that is to say the build config to use to produce binaries compatible with your computer. For example if it is named linux64

    $ qibuild set-host-config linux64
    

    Setting your host config allows QiBuild to select the right binary to run, even when cross-compiling.

  2. Only if you are building libqilang by yourself, build the host tools:

    $ qibuild make-host-tools myservice_impl
    

    Do not forget to call it again every time QiLang source code has changed.

  3. Build your project

    $ qibuild configure myservice_impl
    $ qibuild make myservice_impl
    
  4. Run the tests:

    $ qitest run myservice_impl
    

Running the Service

From your Computer

You can run your service on your computer, while registering it on a robot. It should behave the same as services running locally on the robot.

Note

This is the recommended way to run your service while developing it, but it does not replace automatic tests, nor integration test where the service should be deployed and run on the robot.

Simply run qilaunch to instantiate and register the service, as exposed in myservice_impl/src/myservice_module.cpp:

$ qibuild run -- qilaunch -n MyService -o myservice_module.MyService --qi-url <robot_endpoint>

On a Robot

  1. Cross-compile your service for the robot.
  2. Deploy it:
$ qibuild deploy --runtime --with-tests --url <login@address:/path/to/deploy> myservice_impl
  1. Run it:
$ ssh <login@address>
$ /path/to/deploy/bin/qilaunch -n MyService -o myservice_module.MyService

Using strongly typed proxies in another project

In another project, to benefit from the proxies you generated in myservice, follow these steps:

  1. Add myservice as a buildtime and runtime dependency.
  2. Add myservice as a dependency of your binary:
qi_create_bin(main "main.cpp" DEPENDS qi myservice)
  1. Use the proxy in your code:
#include <qi/applicationsession.hpp>
#include <qi/myservice/myservice.hpp>

qiLogCategory("myclient");

int main(int argc, char* argv[])
{
  qi::ApplicationSession app(argc, argv);
  app.start();

  qi::SessionPtr session = app.session();
  mylibrary::MyServicePtr myserv = session->service("MyService");

  for (const auto& value : myserv->workIt("blah", 42))
    qiLogInfo() << "value: " << value;

  return EXIT_SUCCESS;
}

You can also make asynchronous calls like this:

auto future = myserv->async().workIt("blah", 42);
future.connect(mycallback);

Using a mock-up of the service in another project

When another project depends on the service MyService, it usually requires it for various functionalities, that cannot be tested in absence of MyService. Mocking MyService allows the other project to simulate how MyService behaves, and test the reaction of the project according to that behavior.

QiLang automatically generates GMock classes matching the interfaces provided in the IDL files. GMock is a powerful open-source library for writing mock-ups, that can be tuned specifically for every test unit.

For each IDL file specified to build, a file with the same base name is created under the gmock folder. For example, qi/myservice/myservice.idl.qi will produce qi/myservice/gmock/myservice.hpp.

Here is how to write a test using the GMock classes generated by QiLang, from another project in the same worktree:

  1. Add gmock as a testtime dependency in the qiproject.xml.
  2. In the CMakeLists.txt, create the test binary as following:
qi_create_gmock(
  test_otherproject
  "test_otherproject.cpp"
  DEPENDS myservice
)

In test_otherproject.cpp, typically:

#include <gmock/gmock.h>
#include <qi/session.hpp>
#include <qi/myservice/gmock/myservice.hpp>

// Recreating a session cleanly at every test.
class OtherProjectTest: public testing::Test
{
  void SetUp() override
  {
    _session = qi::makeSession();
    _session->listenStandalone("tcp://127.0.0.1:0");

    // let a mock-up MyService available on the session
    _myService = boost::make_shared<qi::myservice::MyServiceGMock>(_session);
    _session->registerService("MyService", _myService);

    // by default, myService.workIt returns [1, 2, 3]
    ON_CALL(*_myService, workIt(_, _)).WillByDefault(
              testing::Return(qi::Future<std::vector<int>>{std::vector<int>{1, 2, 3}}));

    // ... the things useful to test the project
  }

  void TearDown() override
  {
    _myService.reset();
    _session->close();
    _session.reset();
  }

protected:
  qi::SessionPtr _session;
  boost::shared_ptr<qi::myservice::MyServiceGMock> _myService;
};

TEST_F(OtherProjectTest, whatever)
{
  // ... a test of the other project, indirectly using MyService
}

Distributing

Since the service consists in two separate QiBuild Projects, you can distribute the interface (myservice) and the implementation (myserviceimpl) separately. It is advised to distribute the implementation only on a robot, (in NAOqi Packages, for example) and the interface in an SDK for clients, and consider other services depending on your service like normal clients.

Understanding the Project in depth

myservice project is the only one using QiLang (libqilang) as a host dependency, and it can be seen in myservice/qiproject.xml:

<project name="myproject" version="3">
  <depends buildtime="true" runtime="true" names="libqi"/>
  <depends host="true" names="libqilang"/>
</project>

The IDL file (myservice/qi/myservice/myservice.idl.qi) is where you will write the actual API of your service. The file myservice.idl.qi may look like that:

package mylibrary

interface MyService
  fn emitPing(value: int) // Function
  fn workIt(name: str, value: int) -> Vec<int> // Function with return value
  sig ping(value: int) // Signal
  prop pingEnabled(value: bool) // Property
end

QiLang will be able to generate the code from this IDL file as soon as the following CMake code calls for it. Your CMakeLists.txt should look like this:

cmake_minimum_required(VERSION 2.8)
project(MyService)

find_package(qibuild) # Always required
qi_sanitize_compile_flags(HIDDEN_SYMBOLS) # To behave the same way on every platform
include_directories(".") # Exposes the headers locally

# Generate the specialized proxies. You can put several IDL files if you want.
find_package(qilang-tools) # Required for qi_gen_idl
set(myservice_idl "qi/myservice/myservice.idl.qi")
qi_gen_idl(myservice_generated CPP "qi.myservice" "${CMAKE_CURRENT_BINARY_DIR}" ${myservice_idl})
include_directories(${CMAKE_CURRENT_BINARY_DIR}) # Exposes the generated headers locally

# Install the headers so that people can use your proxies
qi_install_header(
  ${myservice_generated_INTERFACE}
  "qi/myservice/api.hpp"
  KEEP_RELATIVE_PATHS)

# Create a lib with the proxies only
qi_create_lib(myservice
  ${myservice_generated}
  ${myservice_idl} # Makes them visible in the IDE
  DEPENDS
  qi)

qi_stage_lib(myservice)

# Create a header-only library providing GMock mockups, useful to simulate this
# service's behavior from other dependent services.
qi_stage_header_only_lib(
  myservice_gmock
  "${myservice_GMOCK}"
  DEPENDS
  gmock
)

The api.hpp file is still needed and contains:

#ifndef QI_MYSERVICE_API_HPP
#define QI_MYSERVICE_API_HPP

#include <qi/macro.hpp>

#define QI_MYSERVICE_API QI_LIB_API(myservice)

#endif

The argument of QI_LIB_API must be the same as the name of your CMake target, i.e. the library exposing the specialized proxies in this example (“mylibrary”).

You can now focus on the actual implementation of your service, let us have a look at myserviceimpl.hpp:

#include <qi/session.hpp>
#include <src/myservice/myservice_p.hpp>

// Same name as your package in qilang
namespace mylibrary {
  class MyServiceImpl {
  public:
    MyServiceImpl(const qi::SessionPtr& session) : _session(session) { }

    void emitPing(int value) {
      if (pingEnabled.get())
        QI_EMIT ping(value);
    }

    qi::Future<std::vector<int> > workIt(std::string, int) {
      return std::vector<int>{}; // Immediate or asynchronous tasks are advised
    }

    qi::Signal<int> ping;
    qi::Property<bool> pingEnabled;

  private: // This session is useless here, but most people will need it
    qi::SessionPtr _session;
  };
} // mylibrary

// Declare your implementation of MyService
QI_REGISTER_IMPLEMENTATION_H(mylibrary::MyService, mylibrary::MyServiceImpl)

QI_REGISTER_IMPLEMENTATION_H enables the conversion from boost::shared_ptr<MyServiceImpl> to qi::Object<MyService>. It is only needed for the interfaces declared in the IDL, and is not needed for structs, for example.

The last thing to implement is the module associated with your library. It will serve as a cross-language and unified entry point for your service to be started. The code consists in registering types and functions on the module.

#include <qi/anymodule.hpp>
#include "myserviceimpl.hpp"

REGISTER_MYSERVICE(mylibrary::MyServiceImpl);

void registerMe(qi::ModuleBuilder* mb) {
  mb->advertiseFactory<mylibrary::MyService, const qi::SessionPtr&>("MyService");
}

QI_REGISTER_MODULE("mylibrary_module", &registerMe);

Note

It is strongly advised to add tests to your project, to do so you can refer to Using strongly typed proxies in another project.

Subpackages and subfolder in includes

QiLang supports subpackages in the IDL files.

Example, for an IDL file written as mylibrary/subpackage/mydata.idl.qi:

package mylibrary.subpackage

struct MyData
  number: int
  text: str
end

In the CMake, you can refer to this IDL file in addition to the other ones. The generated interface header will be found as <mylibrary/subpackage/mydata.hpp>.

Note

IDL files must always be stored in a directory hierarchy matching the package in which they provide definitions. Here the package is mylibrary.subpackage, so the IDL file must be stored in mylibrary/subpackage.