|
| 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