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 inqi/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¶
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.
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.
Build your project
$ qibuild configure myservice_impl $ qibuild make myservice_impl
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¶
- Cross-compile your service for the robot.
- Deploy it:
$ qibuild deploy --runtime --with-tests --url <login@address:/path/to/deploy> myservice_impl
- 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:
- Add
myservice
as abuildtime and runtime dependency
. - Add
myservice
as a dependency of your binary:
qi_create_bin(main "main.cpp" DEPENDS qi myservice)
- 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:
- Add
gmock
as atesttime dependency
in theqiproject.xml
. - 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", ®isterMe);
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.