GSM Layer 3 Protocol Stack — Parse, Build, and Run a Software BTS in C++20
A type-safe, zero-allocation C++20 library with no external dependencies, spanning the full GSM signalling chain of a software Base Transceiver Station: 216 L3 message classes across 13 PD domains — typed fields for RR/SM/CC/GMM/MM/SS and the SMS CP/RP/TP layers; the BCC/GCC/LS blocks have no normative reference templates, so they are parsed best-effort as opaque bodies and bit-level conformance is not claimed for them — plus passthrough for the PDSS1/PDSS2 and Test-Procedure PDs, the complete LAPDm (L2) entity and A-bis RSL interface, per-subscriber state (context, FSMs, timers, transactions) and ten spec-based protocol procedures — from raw radio bytes up to working MO/MT call flows.
Building a software BTS means implementing the entire Layer-3 signalling stack: parsing hundreds of binary messages, driving protocol state machines, tracking timers, correlating request-response transactions, and generating correct responses at real-time speed. The existing solutions each leave a gap:
- Hand-written C BTS stacks — hand-coded parsers per message type, no builder API, implicit FSMs scattered across handler code;
- Ad-hoc C++ stacks — custom structs with manual byte construction and limited coverage;
- TTCN-3 conformance suites — excellent for validation, unusable as a production protocol stack.
libgsml3parser closes the gap: one type-safe C++20 package that provides everything from raw parse/serialize to per-subscriber procedure state machines — ready to connect to any SDR backend or a BSC over A-bis RSL, with zero third-party dependencies to carry.
| Without libgsml3parser | With libgsml3parser |
|---|---|
| Hand-roll binary parsers for 200+ message types | parseL3Hex("060D41") — one call, typed result |
| Manual byte construction for responses | Fluent builder: .addTMSI(0x12345678, SDCCHType).build() |
| Scatter/gather FSM logic across handlers | Pre-built ProcedureOrchestrator auto-chains Location Update, Auth, Call Setup |
Track timers with raw std::map + cron jobs |
TimerManager — fixed 32-slot arrays, zero allocation; built-in default durations for the 19 named GSM/GPRS timers (T3101, T3102, T3103, T3106, T3108, T3109, T3111, T3112, T3113, T3310, T3311, T3312, T3314, T3315, T3320, T3321, T3322, T3334, T3395); other timer IDs accept a custom duration |
| Correlate request/response with custom TI tables | TransactionManager — O(1) TI lookup, 0.004 µs per match |
| Debug hex dumps by eye | std::format specializations for protocol enums, Expected<T> with bit-position errors |
| Audience | What You Get |
|---|---|
| Software BTS developers | Drop-in L3 layer for a software BTS: parse, build, FSMs, timers, procedures, LAPDm — link gsml3parser and go |
| Protocol testers & fuzzers | Bidirectional binary↔typed API, golden vectors pinned to the normative TS wire layouts, libFuzzer targets for every major entry point |
| SDR / radio hobbyists | Complete L2 (LAPDm) + L3 stack for the Um interface plus A-bis RSL — no networking or SIP dependencies |
Four layers, from raw bits to protocol state:
- L3 Parser & Serializer — hex/bytes ↔ typed
std::variantobjects; compile-timetryGet<T>(); a fluentbuilder()for every message type; zero heap on the hot path (sizeof(ParsedMessage)= 400 B). - LAPDm Protocol Entity — GSM 04.06 / TS 44.064 state machine (SABME/UA/DISC), I-frame segmentation with k=1 and T200 retransmission, contention resolution, 4 KB-bounded reassembly.
- BTS Stack Modules — MSContext, TimerManager (fixed 32-slot arrays, zero allocation; built-in
default durations for the 19 named GSM/GPRS timers (T3101, T3102, T3103, T3106, T3108, T3109,
T3111, T3112, T3113, T3310, T3311, T3312, T3314, T3315, T3320, T3321, T3322, T3334, T3395); other
timer IDs accept a custom duration; O(active) tick),
TransactionManager (O(1) TI index), RR/MM/CC state machines, ChannelPool / ShardedChannelPool,
SubscriberRegistry —
sizeof(SubscriberSession)= 2056 B (10K sessions ≈ 20 MB). - Procedure Framework —
ProcedureRunner+ProcedureOrchestratorauto-chain ten spec-based procedures (Location Update, Authentication, Call Setup MO/MT, Channel Assignment, Ciphering, Paging, Handover, Release, IMSI Detach) through a zero-heapResponseToken→ResponseBuilderpattern; plus A-bis RSL parsing and 14 frame builders for BSC integration.
auto msg = gsml3parser::parseL3Hex("060D41"); // RR Channel Release
if (msg) std::cout << gsml3parser::messageName(*msg); // "ChannelRelease"
auto paging = L3PagingRequestType2::builder()
.addTMSI(0x12345678, ChannelType::SDCCHType).build();// fluent constructionAll 13 protocol domains — RR 98 · SM 24 · CC 21 · GMM 23 · MM 21 · SMS 5 · BCC 8 · GCC 8 · SS 3 · LS 2 · PDSS1 + PDSS2 (Extended) + Test PDs 3: 216 message types in total, with Information Elements and enums defined per domain.
Full catalog (MTIs, directions, IEs, dispatch edge cases such as TIF=1 short messages and build-only types): doc/messages.md.
All parse/build paths for RR/SM/CC/GMM/MM/SS and the SMS CP/RP/TP layers follow the normative 3GPP TS wire layouts; golden vectors are pinned in the test suite. The BCC/GCC/LS blocks have no normative reference templates, so they are parsed best-effort as opaque bodies and bit-level conformance is not claimed for them. Highlights of the non-obvious encodings:
Identities (TS 24.008) — LAI/RAI pack the PLMN as [MCC2|MCC1][MNC3/F|MCC3][MNC2|MNC1] BCD octets
plus a 16-bit LAC (plus one RAC octet for RAI); mcc()/mnc() return the digits in natural order.
The mobile identity value starts with [first digit(4)|odd-count(1)|type(3)] and encodes digit pairs
as [next digit or F fill|current digit]; a TMSI starts with the spare 'F' nibble, a zero bit and
type '100'B (first octet 0xF4).
MM (TS 24.008) — in a sequence of half-octet information elements the
first element occupies the low half-octet (bits 1-4). CM Service Request body
starts with one octet: CM service type (four bits) in the low half-octet and
CKSN(3)|reserved(1) in the high; Location Updating Request starts with
[spare(1)|CKSN(3)] + [FOR(1)|spare(1)|LUtype(2)]; Authentication Request and
CM Re-establishment Request carry the CKSN (three bits plus one spare) in the
low half-octet with the high half-octet spare. Authentication Failure (0x1C) is
a bare reject-cause octet plus, for the "Synch failure" cause, the optional AUTS
TLV 0x22; CM Service Prompt (0x25) is one spare(2)|SAPI(2)|PD(4) octet — both
present per TS 24.008 Table 10.2. TMSI Reallocation Command body is exactly the
LAI (five octets, not length-prefixed) followed by the new mobile identity in LV
form (TS 24.008 section 9.2.17). Optional IEs after the mandatory
part are preserved opaquely and re-emitted verbatim.
GMM / SM (TS 44.068) — Attach/RAU Request pack the type (three bits plus the forL3 indicator) in the low half-octet
and the GPRS CKSN (three bits plus one reserved) in the high half-octet; the DRX parameter is two value octets without an identifier and the MS radio access
capability is a mandatory LV. Reject/failure/status messages start with a single bare cause value
octet; RAU Reject appends one force-to-standby octet after the cause, and Detach Accept is that
one octet alone (the low bit is force-to-standby). Attach Accept leads with [forceToStandby(4)][attachResult(4)]
(result: 1 = GPRS attach, 3 = combined IMSI+GPRS), then the periodic RA update timer octet
[units(3)][value(5)] and the radio-priority octet [radioPrioritySMS(4)][radioPriorityTOM8(4)], then the RAI
(six raw PLMN/LAC/RAC octets) and, when a new P-TMSI is allocated, the P-TMSI signature (TV 0x19, three
octets) and the allocated P-TMSI (TLV 0x18, MI form 0xF4 + TMSI); remaining IEs are preserved opaquely. RAU Accept
leads with [spare(4)][updateResult(3)][forceToStandby(1)], then the same timer octet and the RAI in
octets 2–7, then the optional P-TMSI signature / allocated P-TMSI / MS identity / ready-timer IEs and, when included by
the network, the PDP context status (TLV 0x32, two-octet value). P-TMSI Reallocation Command is
positional: allocated P-TMSI LV (length 5, MI form), RAI, one force-to-standby octet, optional
P-TMSI signature. Service Request starts with one octet — GPRS CKSN(3) in the low bits, service type(3) in the
high half-octet — followed by the P-TMSI LV; further IEs (a conforming MS appends the PDP context
status TLV 0x32) are preserved opaquely. GMM Identity Request is a single
[spare(3)|forceToStandby(1)|type(3)] octet. SM ACTIVATE PDP CONTEXT REQUEST carries the NSAPI and the negotiated LLC SAPI as two
octets with the value in the low four bits and the high nibble zero, the requested QoS profile (three mandatory
octets — delay/reliability class, peak/precedence, mean throughput — plus any optional characteristics, preserved opaquely)
and the requested PDP address as positional LVs, the APN as TLV 0x28 when present, PCO as TLV 0x27 and an
optional request type 0xAx; DEACTIVATE PDP CONTEXT REQUEST starts with the SM cause octet followed by an optional
tear-down indicator octet (0x90, flag in bit 0). The PDP type-and-address value is
[type organization(4) = 0x01 IETF | spare(4)][type number(8)] plus, for a static address only, the address
octets — type numbers 0x21 (IPv4) and 0x53 (IPv6), four / sixteen address octets; a dynamic address
carries no address octets, so the LV length is 2 (dynamic), 6 (static IPv4) or 18 (static IPv6). SM MTI
0x50-0x54 are reserved and not assigned to message classes (Table 10.4a).
CC / SMS — CC message types per TS 24.008 Table 10.3 (no unit-data messages); the Cause IE
(IEI 0x08, length 2) value is [0x60|location][0x80|cause] (TS 24.008 section 10.5.1.2), CC PROGRESS
carries the progress indicator as an LV ([0x02][0xE0|location][0x80|progress]), structured 0xDx IEs
are single-octet and Facility is LV. The SMS PD carries CP-DATA/CP-ACK/CP-ERROR plus
CP-STATUS/CP-SUBMIT (TS 24.011); the RP header octet is [reserved(5)=0][RP-MTI(3)] and the MTI is direction
dependent (TS 24.079 section 7.2.1, Table 8.3): uplink Data/Ack/Error/SMMA = 0/2/4/6, downlink = the uplink
value plus one; the TP-DU header octet is [TP-MTI(2)|MMS/RD(1)|VPF(2)|SRI/SRR(1)|UDHI(1)|RP(1)]
(bit positions in the octet value; TP-MTI 2 = status report network→mobile / command mobile→network),
addresses carry the digit count as length followed by TON/NPI and BCD digits, and the user-data length
counts septets for GSM 7-bit text (TS 23.040 section 9).
SS (TS 24.078) — the SS-Data body is a TCAP-style component container
[component tag(1)][component length(1)][component] with tags Invoke=0xA1, ReturnResult=0xA2,
ReturnError=0xA3 and Reject=0xA4; an invoke component is [02 01 invokeId][02 01 operationCode] [30 len {parameters}], and every component form re-emits losslessly.
RR (TS 44.018) — Ciphering Mode Command is exactly one body octet with sC in the low bit:
[sC(1)|algorithm(3)][cR(1)|spare(3)] (section 9.2.24); Power Command is one octet
[reserved(3)=0][msPowerLevel(5)] (section 10.5.2.6) and Power Command And Access Type adds the aTC,
ePC_Mode and fPC_EP bits above the power level (section 10.5.2.7); SI1 carries the cell channel description in
the sixteen-octet variable-length BCCH frequency list form — seven-bit header, ten-bit base ARFCN and
a 111-bit presence map (section 10.5.2.1b) — plus RACH control parameters (three octets) and at most
one rest octet (body 19–20 octets); SI2/SI2bis/SI2ter and the SI5/SI5bis/SI5ter frequency lists use the
same sixteen-octet bitmap form (any seven-bit header is accepted on parse, raw sixteen octets kept for
lossless re-emit); the access-class
bitmap is a 16-bit value written low byte first (AC i ↔ bit i); a set bit bars the class
(emergency = class 10); Paging Request Type 1/2/3 start with the octet [channelNeeded(4)][pageMode(4)]
(channel needed packed as second|first two-bit fields) followed by the identities — two raw TMSIs plus an
optional third identity (TLV 0x17) for Type 2, four raw TMSIs for Type 3 — and Paging Response starts
with [spare(4)][CKSN(4)]; the Immediate Assignment start time (IEI 0x7C) is two value octets packing
T1(5)/T3(6)/T2(5); channel numbers use the five-bit type-and-offset codes ('00001'B Bm ACCH …
'10000'B BCCH, '10001'B RACH, '10010'B PCH+AGCH, PDCH/CBCH/VAMOS extensions) with
channelCodeLm/Sdcch4/Sdcch8 helpers. SI3/SI4 rest octets carry the selection parameters, power
offset, scheduling-if/where, GPRS indicator, UMTS early-CM and SI2quater indicators; a rest region made purely of
the 0x2B padding pattern means no rest octets are present, and sub-octet alignment is padded with that pattern on emit
(sections 10.5.2.35/10.5.2.37); SI4 optionally carries the CBCH mobile allocation (TLV 0x72) after the CBCH channel description
(TV 0x64); SI4 rest octets are optional (zero or more on emit), SI2bis/SI2ter accept at most one /
four rest octets respectively, and SI13 rest octets carry the typed GPRS cell options, mobile
allocation and power-control parameters, padded to the octet boundary.
LAPDm (GSM 04.06 / TS 51.010-1) — a SABME command with the P/F bit cleared is ignored; an initial SABME command is accepted on any SAPI: with a payload it is answered by a UA echoing that payload, and the link enters the contention-resolution state on SAPI 0 but is established directly on every other SAPI; without a payload the link is established with a plain UA. Every UA and DM we send mirrors the P/F of the received command, and a UA or DM response with F cleared is ignored; a DM while awaiting establishment restarts T200 without releasing the link; the retransmission budget allows N200+1 frames before abnormal release; a T200-expired I-frame is retransmitted with P/F set and the current V(R) in N(R).
A-bis RSL (TS 48.058) — the Channel Mode IE (0x06) value is exactly four octets:
[reserved(6)|DTX_d(1)|DTX_u(1)], speed indicator (Speech=0x01, Data=0x02, Signalling=0x03),
channel rate type (Sdcch=0x01, TchF=0x08, TchH=0x09, …, VAMOS extensions 0x88/0x89) and a
union octet (speech algorithm / opaque data rate / 0x00 for signalling). Error causes use the
canonical section 9.3.26 values (RadioLinkFail=0x01, ResUnavail=0x2F, Proto=0x6F,
Interworking=0x7F, …) and reserved gaps are rejected on emit. The Uplink Measurements IE (0x19) is
three value octets — [RFU|DTX_d|rxlev_full(6)], [res(2)|rxlev_sub(6)], [res(2)|rxq_full(3)|rxq_sub(3)]
— with any vendor supplementary bytes preserved verbatim. The Frame Number IE (0x08) packs t1p(5),
t3(6) and t2(5) derived from an absolute TDMA frame number (t1p=(fn/1326)%32, t3=fn%51,
t2=fn%26). The RACH Load IE (0x12) is LV-encoded with an explicit length
octet (0x06) and three big-endian u16 counters (total/busy/access); CCCH LOAD
INDICATION comes in two channel-number variants — PCH+AGCH tn 0 (0x90) with the
Paging Load IE (TV, two-octet value) and RACH tn 0 (0x88) with the RACH Load IE
(LV) — and RF RESOURCE INDICATION / SACCH FILLING / OVERLOAD INDICATION / ERROR
REPORT belong to the TRX MANAGEMENT group (discriminator 0x08) while LOCATION
INFORMATION belongs to the LCS group (0x10). The IE catalog
covers the full section 9 set (0x01–0x3C; 0x1D is not allocated) plus vendor
extensions 0x60–0x63 (Osmo) and the ip.access group 0xE0–0xFE, in which the
IEs with a defined value size — Remote/Local IP (four octets), Remote/Local
port and Connection ID (two octets), RTP payload type(s), speech mode and RTP
CSD format (one octet) — are TV-encoded, connection statistics is LV-encoded
(length octet 0x1C, seven big-endian u32 counters) and the remaining codes
have no defined size and decode as LV;
TL16V for L3Info (0x0B) only, and LV for everything else — including Full
BCCH Info (0x27). The channel number codes include the VAMOS Lm sub-slot extensions
(30/31); MEASUREMENT RESULT carries the always-present BS_POWER TV IE between Uplink
Measurements and L1 Information.
cmake -S . -B build -DBUILD_TESTS=ON -DBUILD_EXAMPLES=ON
cmake --build build --config Release --parallel
ctest --test-dir build --output-on-failure # 2100+ testsRequirements: C++20 (MSVC 2022 17.x, GCC 13+, or Clang with a std::format-capable stdlib) and CMake
3.20. All build options (BUILD_SHARED_LIBS, ENABLE_FUZZING, ENABLE_ASAN, …), the find_package()
consumer pattern, the error-handling model, and the full API tour: doc/API.md.
Numbers that matter for a real-time radio stack (Release, single core):
| Metric | Result |
|---|---|
| L3 parse throughput (per message type) | 7.7 – 26.5 M msg/s |
| Mixed-domain stream (all 13 PDs) / zero-copy | 10.8 / 9.7 M msg/s |
| Full BTS stack dispatch (1K MS: timers, FSMs, correlation) | ~66 – 86 M msg/s |
| TimerManager tick / Transaction match | 42.6 M ticks/s / 0.004 µs per match |
| 1M and 2M sessions (create / TMSI lookup / tick 10K active) | 1195 / 127 / 1.4 ms and 1096 / 258 / 1.4 ms |
example_rt_scale: 1M sessions, real-time event loop |
6.2 M msg/s aggregate, 0 overruns / 1000 ticks, 2.0 GB peak |
Every benchmark and stress test prints the dynamically detected hardware ID (CPU, caches, RAM, OS), so
results are attributed to the machine that produced them; a full annotated run:
benchmark_results.txt. Verify on your own machine with example_benchmark,
example_benchmark_stack and example_rt_scale. Optimization techniques behind the numbers:
doc/bts_architecture.md §6, doc/API.md §62.
| Aspect | Hand-written C stack | Ad-hoc C++ stack | libgsml3parser |
|---|---|---|---|
| Language | C | C++ (manual memory) | C++20 |
| Type safety | enum + manual cast | custom structs | std::variant + tryGet<T>() — compile-time, no RTTI |
| Message types | hand-coded per message | partial coverage | 216 typed messages, all 13 PD domains |
| Builder API | none (manual struct) | partial | fluent builder for every type |
| FSM + timers + correlation | implicit in handlers | custom | built-in stack modules + procedure framework |
| LAPDm / A-bis RSL | separate library | custom | full LAPDm entity + RSL parse/build included |
| Dependencies | external core library | multiple | zero (C++20 stdlib only) |
Every detail lives in a dedicated guide; this README is the pitch and the index.
| Document | What It Covers |
|---|---|
| doc/API.md | Full API reference (63 numbered sections): core types, bit I/O, streaming, parser/serializer, builders and IEs/enums of all 13 domains, LAPDm, dispatcher, arena, every stack module, RSL, all procedures, C ABI, FFI bindings + spec conformance notes |
| doc/bts_integration.md | Primary guide for BTS developers: step-by-step event loop with ProcedureOrchestrator, full worked procedure chains (Location Update, Call Setup MO, Paging), AuC/VLR/BSC typed-data integration, LAPDm link management, L3 timer reference table, SI broadcast, production error handling |
| doc/bts_architecture.md | Two usage modes (L3 Parser vs BTS Stack), component & data-flow diagrams, PHY/SDR integration points, thread-safety matrix, per-MS memory footprint, allocation-free hot paths, scaling guidelines to millions of sessions |
| doc/messages.md | Complete message catalog: all 216 types with MTIs and directions, CC/GMM/SM IEs, SMS CP/RP/TP layers, dispatch edge cases (TIF=1 short messages, build-only types) |
| doc/boundaries.md | What the library intentionally excludes — PHY/SDR, speech codecs, A5 ciphering, OML, SIP/media gateways, PS full stack, configuration, logging — and the exact integration point for each |
| examples/ | 20 runnable demos (see below), incl. full BTS flows, benchmarks, and a 1M-session real-time loop |
| bindings/README.md | FFI bindings (Python / Go / Rust) over the stable C ABI: unified quickstarts, ownership & threading model, callback safety rules, extension guide, test gate |
| bindings/python/README.md, bindings/go/README.md, bindings/rust/README.md | Per-language binding guides with identical structure: layout, building & linking, quickstart, error model, ownership & callbacks, versioning |
| Example | Description |
|---|---|
example_parse_file |
Parse L3 from hex strings/files, typed access via tryGet<>, round-trip serialization |
example_streaming |
L3Framer + stream processing over SpanByteSource and a RingBuffer producer/consumer |
example_multithread |
Concurrent parsing across all 13 PD domains with immutable configs (no mutex) |
example_zero_copy |
InlineFramer + ZeroCopyStreamProcessor on a contiguous buffer (DMA/PCAP-style) |
example_benchmark / example_benchmark_stack |
Component benchmarks with hardware-ID reporting |
example_lapdm_entity |
Full LAPDm lifecycle: SABME/UA, UI + segmented I-frame data, T200 retransmission, DISC |
example_bts_paging |
Paging cycle: Builder → L3 bytes → LAPDm UI frame → unwrap → parse → verify |
example_bts_channel_assignment |
Channel Request on RACH → ImmediateAssignment response, full round-trip |
example_bts_rach_assignment |
RACH → channel allocation → assignment → MS response using stack modules |
example_bts_sysinfo |
System Information (SI1–SI4) construction for BCCH broadcast |
example_bts_dispatcher |
ProtocolDispatcher routing: specific, domain fallback, TI-based handlers |
example_bts_location_update |
Location updating flow: CM Service Request, identity verification, TMSI reallocation |
example_bts_paging_call |
Paging → channel assignment → MM auth → CC call setup |
example_subscriber_registry |
Registry workflow: create/assign/tick/remove subscriber sessions |
example_rt_scale |
1M sessions, 10 ms event-loop ticks, real-time overrun verification |
example_procedure_location_update |
Full Location Update procedure via ProcedureRunner |
example_procedure_call_setup |
Mobile-Originated Call Setup via ProcedureRunner |
example_rsl_pipeline |
A-bis RSL in → L3 out (incl. TL16V L3Info wrapping) and RSL response out |
example_reference_bts |
Location Update + MO Call Setup chains through ProcedureOrchestrator with ShardedSubscriberRegistry |
The full stack is also reachable through a stable C89 C ABI
(include/gsml3parser/gsml3parser_c.h, contract in doc/API.md §63).
Three first-party bindings share one set of semantics — RAII ownership, the borrowed-session rule,
queue-model callbacks — and carry no third-party runtime dependencies:
| Language | Mechanism | Coverage | Runnable demo |
|---|---|---|---|
| Python | ctypes, stdlib only |
entire C ABI: 238 functions, completeness pinned by test | bts_simulation.py |
| Go | cgo + cgo.Handle callback bridges (needs a C toolchain) |
v1 surface: 135 functions incl. typed L3 builders | cmd/gsmexample |
| Rust | handwritten sys crate + safe wrapper — Send, no bindgen/codegen |
same 135-function v1 surface; extern block checked at compile time | bts_simulation.rs |
All three run the same "MO call over SDCCH" scenario against the shared C core, and one unified gate
(scripts/verify_bindings.ps1, CI on Linux + Windows) builds and tests them together. Per-language
guides (quickstart, building & linking, error model, ownership & callbacks):
bindings/README.md plus the README.md in each binding directory.
Quality you can verify, not just trust:
- GoogleTest suite: 2100+ tests — parse/golden-vector (normative TS wire layouts), round-trip, builders, LAPDm FSM, dispatcher, all procedures and orchestrator chains, RSL, C ABI + strict-C89 header check, high-load stress (1M/2M sessions) and concurrency tests.
ENABLE_FUZZING=ON(Clang/LLVM) builds eight libFuzzer targets (L3 parse, RSL parse, LAPDm frame decode, LAPDm entity, L3Framer, orchestrator, subscriber registry, C API) with ASan/UBSan; CI adds a ThreadSanitizer pass on Linux and attaches a prebuilt static+shared package to each GitHub Release.
Parse/build/RSL entry points are stateless; per-session stack modules are single-thread by design (the
one-instance-per-MS event-loop model keeps the hot path lock-free); ShardedSubscriberRegistry and
ShardedChannelPool provide the thread-safe variants. Complete matrix:
doc/bts_architecture.md §5.
MIT License. See COPYING for details. Golden test vectors are pinned to the normative 3GPP TS wire layouts (TS 24.008, TS 44.018, TS 44.064, TS 48.058 and friends). Copyright 2026 momentics <momentics@gmail.com> and libgsml3parser contributors.