Skip to content

Commit 7a5971e

Browse files
ashtumlouistatta
authored andcommitted
Mohammad 2025 Q2
1 parent 69e7d98 commit 7a5971e

File tree

1 file changed

+270
-0
lines changed

1 file changed

+270
-0
lines changed
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
---
2+
layout: post
3+
nav-class: dark
4+
categories: mohammad
5+
title: 'Boost.RunTimeServices: The Glue for Optional Runtime Features'
6+
author-id: mohammad
7+
---
8+
9+
## How Boost.RunTimeServices Emerged from Boost.HTTP.Proto Development
10+
11+
During the development of the
12+
[**Boost.HTTP.Proto**](https://github.com/cppalliance/http_proto) library, we
13+
recognized the need for a flexible mechanism to install and access optional
14+
services at runtime without requiring prior knowledge of their specific
15+
implementations. For example, building a library with optional support
16+
for zlib and Brotli compression, even if those libraries weren’t installed on
17+
the user's machine. This challenge led to the creation of
18+
[**Boost.RunTimeServices**](https://github.com/cppalliance/rts), a solution that
19+
offers several key benefits to both library developers and users, which I will
20+
briefly outline below.
21+
22+
#### Libraries With No Configuration Macros
23+
24+
One approach to managing optional dependencies in libraries is to use
25+
configuration macros at build time, such as `BOOST_HTTP_PROTO_HAS_ZLIB` or
26+
`BOOST_COOKIES_HAS_PSL`. However, this approach has major drawbacks:
27+
28+
1. Combinatorial explosion of binary variants.
29+
2. Users can't easily determine which features are enabled in a binary.
30+
3. Configuration macros leak into downstream libraries, compounding complexity.
31+
4. Changing features requires full rebuilds of all dependent code.
32+
5. Difficult to distribute a single binary via package managers.
33+
34+
With **Boost.RunTimeServices**, configuration macros become unnecessary.
35+
Features can be queried and installed at runtime. For example, installing an
36+
optional zlib inflate service:
37+
38+
```CPP
39+
rts::context rts_ctx;
40+
rts::zlib::install_inflate_service(rts_ctx);
41+
```
42+
43+
Then, a library can conditionally use the service:
44+
45+
```CPP
46+
if(cfg.decompression)
47+
{
48+
auto& svc = ctx.get_service<rts::zlib::inflate_service>();
49+
svc.inflate(stream, rts::zlib::flush::finish);
50+
}
51+
```
52+
53+
#### Smaller Binaries by Stripping Unused Features
54+
55+
Since service interfaces are decoupled from implementations, unused services and
56+
their dependencies can be eliminated by the linker. For example the following is
57+
part of the implementation of `rts::zlib::inflate_service`:
58+
59+
```CPP
60+
class inflate_service_impl
61+
: public inflate_service
62+
{
63+
public:
64+
using key_type = inflate_service;
65+
66+
int
67+
init2(stream& st, int windowBits) const override
68+
{
69+
stream_cast sc(st);
70+
return inflateInit2(sc.get(), windowBits);
71+
}
72+
73+
int
74+
inflate(stream& st, int flush) const override
75+
{
76+
stream_cast sc(st);
77+
return ::inflate(sc.get(), flush);
78+
}
79+
80+
// ...
81+
}
82+
```
83+
84+
The implementation class is only instantiated within:
85+
86+
```CPP
87+
inflate_service&
88+
install_inflate_service(context& ctx)
89+
{
90+
return ctx.make_service<inflate_service_impl>();
91+
}
92+
```
93+
94+
Libraries interact only with the abstract interface:
95+
96+
```CPP
97+
struct BOOST_SYMBOL_VISIBLE
98+
inflate_service
99+
: public service
100+
{
101+
virtual int init2(stream& st, int windowBits) const = 0;
102+
virtual int inflate(stream& st, int flush) const = 0;
103+
// ...
104+
};
105+
```
106+
107+
If the user never calls `install_inflate_service`, the implementation and its
108+
dependencies are omitted from the binary.
109+
110+
In this particular example, having separate services for inflation and deflation
111+
gives us more granularity on the matter. For instance, a client
112+
application that uses **Boost.HTTP.Proto** will more likely only need to install
113+
`rts::zlib::inflate_service`, because it typically only needs to parse
114+
compressed HTTP response messages and compression of HTTP requests almost never
115+
happens in client applications. The reverse is true for server applications and
116+
they might only need to install `rts::zlib::deflate_service`, since client
117+
requests usually arrive uncompressed and the server needs to compress responses
118+
(if requested).
119+
120+
#### Libraries Built Independent of the Availability of Optional Services
121+
122+
Because a library that uses an optional service needs only the interface of that
123+
service, there is no need for a build-time dependency. Therefore, we can always
124+
build a single version of a library that takes advantage of all optional
125+
services if they are available at runtime.
126+
127+
For example, in the case of **Boost.HTTP.Proto**, one can use the library
128+
without any compression services, as users simply don’t install those services
129+
and there’s no need to link any extra libraries.
130+
131+
Another user can use the exact same binary of **Boost.HTTP.Proto** with zlib and
132+
Brotli decompression algorithms:
133+
134+
```CPP
135+
rts::context rts_ctx;
136+
rts::zlib::install_inflate_service(rts_ctx); // links against boost_rts_zlib
137+
rts::brotli::install_decoder_service(rts_ctx); // links against boost_rts_brotli
138+
```
139+
140+
#### Optional Services in Downstream Libraries
141+
142+
Assume we want to create a library named **Boost.Request** that uses
143+
**Boost.HTTP.Proto** and **Boost.HTTP.IO**, and provides an easy-to-use
144+
interface for client-side usage. Such a library doesn't need to care about
145+
optional services and can delegate that responsibility to the end user, allowing
146+
them to decide which services to install. For example, **Boost.Request** can
147+
internally query the availability of these services and make requests
148+
accordingly:
149+
150+
```CPP
151+
if(rts_ctx.has_service<brotli::decoder_service>())
152+
encodings.append("br");
153+
154+
if(rts_ctx.has_service<zlib::inflate_service>())
155+
{
156+
encodings.append("deflate");
157+
encodings.append("gzip");
158+
}
159+
160+
if(!accept_encoding.empty())
161+
request.set(field::accept_encoding, encodings.str());
162+
```
163+
164+
## Why This Needs to Be a Separate Library
165+
166+
This is a core library that many other libraries may want to use. For example, a
167+
user who installs zlib services expects them to be usable in both
168+
**Boost.HTTP.Proto** and **Boost.WS.Proto**:
169+
170+
```cpp
171+
rts::context rts_ctx;
172+
rts::zlib::install_inflate_service(rts_ctx);
173+
rts::zlib::install_deflate_service(rts_ctx);
174+
175+
// Usage site
176+
http_proto::parser parser(rts_ctx);
177+
ws_proto::stream stream(rts_ctx);
178+
```
179+
180+
User libraries need to link against `boost_rts` in order to access
181+
`rts::context`. Note that `boost_rts` is a lightweight target with no dependency
182+
on optional services like zlib or Brotli.
183+
184+
## Existing Challenges
185+
186+
#### Minimum Library For Mandatory Symbols
187+
188+
A library that uses an optional service might still need to link against a
189+
minimal version that provides necessary symbols such as `error_category`
190+
instances, because we usually need to instantiate them inside the source and
191+
can't leave them in headers.
192+
193+
For example, assume a library that needs to call an API to provide the error
194+
message:
195+
196+
```CPP
197+
char const*
198+
error_cat_type::
199+
message(
200+
int ev,
201+
char*,
202+
std::size_t) const noexcept
203+
{
204+
return c_api_get_error_message(ev);
205+
}
206+
```
207+
208+
This clearly can't be left in the headers because it would require the existence
209+
of the `c_api_get_error_message` symbol at link time, which defeats the purpose
210+
of optional services.
211+
212+
To allow optional linkage, a fallback could be provided:
213+
214+
```CPP
215+
char const*
216+
error_cat_type::
217+
message(
218+
int ev,
219+
char*,
220+
std::size_t) const noexcept
221+
{
222+
return "service not available";
223+
}
224+
```
225+
226+
But the remaining question is: where should this implementation go if we want
227+
optional linkage against services? Currently, we place this code inside the core
228+
**Boost.RunTimeServices** library, which could become a scalability problem in
229+
the future as the number of services grows.
230+
231+
#### An Even Finer Grain Control Over Used and Unused Symbols
232+
233+
Even though separate services (e.g., `inflate_service`, `deflate_service`) help
234+
the linker remove unused code; the granularity is still limited. For example, if
235+
a library uses only `inflate_service::init`, the linker still includes
236+
`inflate_service::init2` and other unused methods. This is because interfaces are
237+
polymorphic and the linker can't remove individual virtual methods:
238+
239+
240+
```CPP
241+
class inflate_service_impl
242+
: public inflate_service
243+
{
244+
public:
245+
using key_type = inflate_service;
246+
247+
int
248+
init(stream& st) const override
249+
{
250+
stream_cast sc(st);
251+
return inflateInit(sc.get());
252+
}
253+
254+
int
255+
init2(stream& st, int windowBits) const override
256+
{
257+
stream_cast sc(st);
258+
return inflateInit2(sc.get(), windowBits);
259+
}
260+
261+
// ...
262+
}
263+
```
264+
265+
#### Space Overhead and Indirection Cost of Virtual Services
266+
267+
This is probably not an issue for most users, as these costs are negligible in
268+
real-world applications. However, a solution that provides the same
269+
functionality as virtual service interfaces but without these overheads would be
270+
highly desirable.

0 commit comments

Comments
 (0)