Warning
qilang is still under active development and its API may not be backward-compatible in the future.
qilang allows the usage of strongly-typed proxies in client code. This means that instead of writing:
std::vector<int> ret = myservice.call<std::vector<int> >("workIt", "blah", 12);
and maybe trigger a run-time error because the argument types are wrong, using qilang allows you to write:
std::vector<int> ret = myservice->workIt("blah", 12);
and get a compile-time error in case you got the argument types incorrect.
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.
A typical project using qilang would look like this:
myproject
├── mylibrary
│ ├── api.hpp
│ └── myservice.idl.qi
├── src
│ ├── myserviceimpl.hpp
│ └── mylibrary_module.cpp
├── qiproject.xml
└── CMakeLists.txt
You can find a template in <repo>/templates/service and create your project with this single command-line:
$ qisrc create -i <repo>/templates/service-cpp myservice
The project, library and service names will be identical in this precise case.
qilang takes advantage of libqi to generate the specialized proxy boilerplate out of the provided .idl.qi file, hence it needs to be compiled for your machine, which will execute the generator, rather than for your target device: this is called host dependency.
Note
When cross-compiling, a host dependency will still be compiled for your system, instead of being compiled for the target system.
Host and regular dependencies will appear as following in your qiproject.xml:
<project name="myproject" version="3">
<depends buildtime="true" runtime="true" names="libqi"/>
<depends host="true" names="libqilang"/>
</project>
The IDL file is where you will write the actual API of your service. The file myservice.idl.qi could 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(mymodule)
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(mylibrary_idl "mylibrary/myservice.idl.qi")
qi_gen_idl(mylibrary_generated CPP "mylibrary" "${CMAKE_CURRENT_BINARY_DIR}" ${mylibrary_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(
${mylibrary_generated_INTERFACE}
"mylibrary/api.hpp"
KEEP_RELATIVE_PATHS)
# Create a lib with the proxies only
qi_create_lib(mylibrary
${mylibrary_generated}
${mylibrary_idl}
DEPENDS
qi)
qi_stage_lib(mylibrary)
# Create a module with your implementation of the MyService interface
# It is the easiest way to expose your service in NAOqi
find_package(qimodule) # Required only if you create a module
qi_create_module(
mylibrary_module
SRC
"src/myserviceimpl.hpp"
"mylibrary_module.cpp"
DEPENDS qi mylibrary
)
The api.hpp file is still needed and must contain:
#ifndef MYLIBRARY_API_HPP
#define MYLIBRARY_API_HPP
#include <qi/macro.hpp>
#define MYLIBRARY_API_HPP QI_LIB_API(mylibrary)
#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_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 will automatically enable 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);
Good job, but now it is time to face your compiler =) It is strongly advised to add tests to your project, to do so you can refer to Using strongly typed proxies in another project.
$ qibuild set-host-config linux64
This has to be done only once ever, usually.
$ qibuild make-host-tools
Do not forget to call it again if you update qilang, for example when syncing a worktree including it.
$ qibuild configure
$ qibuild make
In another project, to benefit from the proxies you generated in myproject, follow these steps:
mylibrary
as a dependency of your binary:qi_create_bin(main "main.cpp" DEPENDS qi mylibrary)
#include <qi/applicationsession.hpp>
#include <mylibrary/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 0;
}
You can also make asynchronous calls like this:
auto future = myserv->async().workIt("blah", 42);
future.connect(mycallback);
qilang also supports subpackages in the IDL files, and it has an impact in how the headers will be exposed. Say you have the following IDL file 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 in <mylibrary/subpackage/mydata.hpp>.
Note
IDL files must always be stored in a directories matching the package in which they provide definitions. Here the package is mylibrary.subpackage, so the IDL file must be stored in mylibrary/subpackage.