|
| 1 | +# ACN Deferred Patching (`--acn-v2`) |
| 2 | + |
| 3 | +Deferred patching is an experimental code generation strategy for ACN encoding that eliminates temporary buffers, produces thread-safe code, and generates smaller, more readable C functions. |
| 4 | + |
| 5 | +## The Problem |
| 6 | + |
| 7 | +Consider a typical satellite protocol PDU where a header carries length and size fields that describe data appearing later in the bitstream: |
| 8 | + |
| 9 | +**ASN.1 definition:** |
| 10 | + |
| 11 | +```asn1 |
| 12 | +MyPDU ::= SEQUENCE { |
| 13 | + header MyHeader, |
| 14 | + payload OCTET STRING (CONTAINING MyPayload) |
| 15 | +} |
| 16 | +
|
| 17 | +MyHeader ::= SEQUENCE {} |
| 18 | +
|
| 19 | +MyPayload ::= SEQUENCE { |
| 20 | + a INTEGER (0..1024), |
| 21 | + buffer1 OCTET STRING (SIZE(0..100)) |
| 22 | +} |
| 23 | +``` |
| 24 | + |
| 25 | +**ACN annotations:** |
| 26 | + |
| 27 | +``` |
| 28 | +MyPDU [] { |
| 29 | + header [] { |
| 30 | + payload-length INTEGER [encoding pos-int, size 16], |
| 31 | + buffer1-length INTEGER [encoding pos-int, size 8] |
| 32 | + }, |
| 33 | + payload [size header.payload-length] { |
| 34 | + a [encoding pos-int, size 32], |
| 35 | + buffer1 [size header.buffer1-length] |
| 36 | + } |
| 37 | +} |
| 38 | +``` |
| 39 | + |
| 40 | +The header contains two ACN-inserted determinant fields: `payload-length` (16 bits) controls the size of the entire `payload` blob, and `buffer1-length` (8 bits) controls the size of `buffer1` inside the payload. Both values must be written to the bitstream *before* the payload data, but their values are only known *after* encoding the payload. |
| 41 | + |
| 42 | +### Legacy generated code |
| 43 | + |
| 44 | +Without `--acn-v2`, the compiler solves this chicken-and-egg problem by encoding the payload to a temporary buffer first, measuring the result, then writing the determinant values and copying the bytes: |
| 45 | + |
| 46 | +```c |
| 47 | +flag MyPDU_ACN_Encode(const MyPDU* pVal, BitStream* pBitStrm, |
| 48 | + int* pErrCode, flag bCheckConstraints) |
| 49 | +{ |
| 50 | + flag ret = TRUE; |
| 51 | + asn1SccUint MyPDU_header_payload_length; |
| 52 | + asn1SccUint MyPDU_header_buffer1_length; |
| 53 | + |
| 54 | + /* Problem 1: static buffer -- not thread-safe, not reentrant */ |
| 55 | + static byte arr[MyPayload_REQUIRED_BYTES_FOR_ACN_ENCODING]; |
| 56 | + BitStream bitStrm; |
| 57 | + |
| 58 | + /* Step 1: encode the ENTIRE payload to a temporary bitstream */ |
| 59 | + BitStream_Init(&bitStrm, arr, sizeof(arr)); |
| 60 | + BitStream* pBitStrm_save = pBitStrm; |
| 61 | + pBitStrm = &bitStrm; |
| 62 | + |
| 63 | + /* Problem 3: parent function reaches directly into child fields */ |
| 64 | + Acn_Enc_Int_PositiveInteger_ConstSize_big_endian_32(pBitStrm, pVal->payload.a); |
| 65 | + ret = BitStream_EncodeOctetString_no_length(pBitStrm, |
| 66 | + pVal->payload.buffer1.arr, pVal->payload.buffer1.nCount); |
| 67 | + pBitStrm = pBitStrm_save; |
| 68 | + |
| 69 | + /* Step 2: compute determinant values from the temporary encoding */ |
| 70 | + MyPDU_header_payload_length = bitStrm.currentBit == 0 |
| 71 | + ? bitStrm.currentByte : (bitStrm.currentByte + 1); |
| 72 | + MyPDU_header_buffer1_length = pVal->payload.buffer1.nCount; |
| 73 | + |
| 74 | + /* Step 3: write determinant values to the real stream */ |
| 75 | + Acn_Enc_Int_PositiveInteger_ConstSize_big_endian_16(pBitStrm, |
| 76 | + MyPDU_header_payload_length); |
| 77 | + Acn_Enc_Int_PositiveInteger_ConstSize_8(pBitStrm, |
| 78 | + MyPDU_header_buffer1_length); |
| 79 | + |
| 80 | + /* Step 4: copy the already-encoded payload bytes -- Problem 2: double encoding */ |
| 81 | + ret = BitStream_EncodeOctetString_no_length(pBitStrm, |
| 82 | + arr, (int)MyPDU_header_payload_length); |
| 83 | + |
| 84 | + return ret; |
| 85 | +} |
| 86 | +``` |
| 87 | +
|
| 88 | +This approach has three problems: |
| 89 | +
|
| 90 | +1. **Static buffer** (`static byte arr[...]`) -- the temporary buffer is `static`, making the function non-reentrant and not thread-safe. Using a stack buffer instead risks stack overflow for large payloads. |
| 91 | +
|
| 92 | +2. **Double encoding** -- the payload data is encoded twice: first to the temporary buffer (to learn its size), then the raw bytes are bulk-copied to the real bitstream. |
| 93 | +
|
| 94 | +3. **Monolithic function** -- the parent `MyPDU_ACN_Encode` directly accesses child type fields (`pVal->payload.a`, `pVal->payload.buffer1.arr`). No calls to `MyPayload_ACN_Encode` are generated. Everything is inlined into one large function that is hard to read and debug. |
| 95 | +
|
| 96 | +
|
| 97 | +## The Solution: Deferred Patching |
| 98 | +
|
| 99 | +Instead of computing determinant values upfront, deferred patching uses a three-step approach: |
| 100 | +
|
| 101 | +1. **Reserve space** -- write placeholder bits at the determinant's position and save that position |
| 102 | +2. **Encode data** -- encode the payload fields directly to the real bitstream (single pass) |
| 103 | +3. **Patch** -- seek back to the saved position, write the now-known determinant value, and restore the stream position |
| 104 | +
|
| 105 | +### Runtime primitives |
| 106 | +
|
| 107 | +The C runtime provides three building blocks: |
| 108 | +
|
| 109 | +```c |
| 110 | +/* Holds a saved bitstream position + the determinant value */ |
| 111 | +typedef struct { |
| 112 | + AcnBitStreamPos pos; /* where in the stream the determinant lives */ |
| 113 | + flag is_set; /* has the value been written? (for shared determinants) */ |
| 114 | + asn1SccUint value; /* the determinant value */ |
| 115 | +} AcnInsertedFieldRef; |
| 116 | +
|
| 117 | +/* Reserve space: write 'size' zero bits, save position in 'det' */ |
| 118 | +void Acn_InitDet_<name>(BitStream* pBitStrm, AcnInsertedFieldRef* det); |
| 119 | +
|
| 120 | +/* Patch: seek back to det->pos, write value 'v', restore stream position */ |
| 121 | +flag Acn_PatchDet_<name>(asn1SccUint v, BitStream* pBitStrm, |
| 122 | + AcnInsertedFieldRef* det, int* pErrCode); |
| 123 | +``` |
| 124 | + |
| 125 | +The `<name>` suffix matches the encoding class -- for example, `Acn_InitDet_U16_BE` / `Acn_PatchDet_U16_BE` for a 16-bit big-endian unsigned integer, or `Acn_InitDet_U8` / `Acn_PatchDet_U8` for an 8-bit unsigned. |
| 126 | + |
| 127 | +### Generated code with `--acn-v2` |
| 128 | + |
| 129 | +With deferred patching, the compiler produces three focused functions instead of one monolithic block: |
| 130 | + |
| 131 | +**Parent function** -- a thin orchestrator: |
| 132 | + |
| 133 | +```c |
| 134 | +flag MyPDU_ACN_Encode(const MyPDU* pVal, BitStream* pBitStrm, |
| 135 | + int* pErrCode, flag bCheckConstraints) |
| 136 | +{ |
| 137 | + flag ret = TRUE; |
| 138 | + AcnInsertedFieldRef buffer1_length; |
| 139 | + AcnInsertedFieldRef payload_length; |
| 140 | + |
| 141 | + /*Encode header*/ |
| 142 | + ret = MyPDU_header_ACN_Encode(&pVal->header, pBitStrm, pErrCode, FALSE, |
| 143 | + &buffer1_length, &payload_length); |
| 144 | + if (ret) { |
| 145 | + /*Encode payload*/ |
| 146 | + ret = MyPDU_payload_ACN_Encode(&pVal->payload, pBitStrm, pErrCode, FALSE, |
| 147 | + &buffer1_length, &payload_length); |
| 148 | + } |
| 149 | + return ret; |
| 150 | +} |
| 151 | +``` |
| 152 | +
|
| 153 | +The parent declares `AcnInsertedFieldRef` structs on the stack and passes them by pointer to the child functions. No temporary buffers, no direct access to child type fields. |
| 154 | +
|
| 155 | +**Header encoder** -- reserves space for determinants: |
| 156 | +
|
| 157 | +```c |
| 158 | +flag MyPDU_header_ACN_Encode(const MyHeader* pVal, BitStream* pBitStrm, |
| 159 | + int* pErrCode, flag bCheckConstraints, |
| 160 | + AcnInsertedFieldRef* MyPDU_header_buffer1_length, |
| 161 | + AcnInsertedFieldRef* MyPDU_header_payload_length) |
| 162 | +{ |
| 163 | + flag ret = TRUE; |
| 164 | +
|
| 165 | + /* Reserve 16 bits for payload-length, save position */ |
| 166 | + Acn_InitDet_U16_BE(pBitStrm, MyPDU_header_payload_length); |
| 167 | + if (ret) { |
| 168 | + /* Reserve 8 bits for buffer1-length, save position */ |
| 169 | + Acn_InitDet_U8(pBitStrm, MyPDU_header_buffer1_length); |
| 170 | + } |
| 171 | + return ret; |
| 172 | +} |
| 173 | +``` |
| 174 | + |
| 175 | +**Payload encoder** -- encodes data, then patches determinant values: |
| 176 | + |
| 177 | +```c |
| 178 | +flag MyPDU_payload_ACN_Encode(const MyPayload* pVal, BitStream* pBitStrm, |
| 179 | + int* pErrCode, flag bCheckConstraints, |
| 180 | + AcnInsertedFieldRef* MyPDU_payload_buffer1_length, |
| 181 | + AcnInsertedFieldRef* MyPDU_payload_payload_length) |
| 182 | +{ |
| 183 | + flag ret = TRUE; |
| 184 | + |
| 185 | + { |
| 186 | + AcnBitStreamPos acn_data_start = Acn_BitStream_GetPos(pBitStrm); |
| 187 | + |
| 188 | + /*Encode a*/ |
| 189 | + Acn_Enc_Int_PositiveInteger_ConstSize_big_endian_32(pBitStrm, pVal->a); |
| 190 | + if (ret) { |
| 191 | + /*Encode buffer1*/ |
| 192 | + ret = BitStream_EncodeOctetString_no_length(pBitStrm, |
| 193 | + pVal->buffer1.arr, pVal->buffer1.nCount); |
| 194 | + } |
| 195 | + |
| 196 | + if (ret) { |
| 197 | + /* Measure how many bytes were written */ |
| 198 | + AcnBitStreamPos acn_data_end = Acn_BitStream_GetPos(pBitStrm); |
| 199 | + asn1SccUint acn_nCount = Acn_BitStream_DistanceInBytes( |
| 200 | + acn_data_start, acn_data_end); |
| 201 | + /* Patch payload-length back in the header */ |
| 202 | + ret = Acn_PatchDet_U16_BE((asn1SccUint)acn_nCount, pBitStrm, |
| 203 | + MyPDU_payload_payload_length, pErrCode); |
| 204 | + } |
| 205 | + } |
| 206 | + |
| 207 | + /* Patch buffer1-length back in the header */ |
| 208 | + ret = Acn_PatchDet_U8((asn1SccUint)pVal->buffer1.nCount, pBitStrm, |
| 209 | + MyPDU_payload_buffer1_length, pErrCode); |
| 210 | + |
| 211 | + return ret; |
| 212 | +} |
| 213 | +``` |
| 214 | +
|
| 215 | +The payload encoder writes field data directly to the real bitstream in a single pass. After encoding, it measures the byte distance (for `CONTAINING` size) and patches both determinant values back into their reserved positions in the header. |
| 216 | +
|
| 217 | +### Shared determinant consistency |
| 218 | +
|
| 219 | +When the same determinant is consumed by multiple fields, `Acn_PatchDet_*` performs a consistency check: if `is_set` is already true, it verifies that the new value matches the previously written one. A mismatch returns `ERR_ACN_DET_CONSISTENCY_MISMATCH`. |
| 220 | +
|
| 221 | +
|
| 222 | +## How to Use It |
| 223 | +
|
| 224 | +Add `--acn-v2` to the `asn1scc` command line: |
| 225 | +
|
| 226 | +```bash |
| 227 | +asn1scc -c -ACN --acn-v2 -atc -o out/ myfile.asn1 myfile.acn |
| 228 | +``` |
| 229 | + |
| 230 | +The alternate flag name `-acnDeferred` is also accepted. |
| 231 | + |
| 232 | +**Limitations:** |
| 233 | +- C backend only (experimental) |
| 234 | +- When `--acn-v2` is omitted, the compiler produces identical output to before -- there is no regression |
| 235 | + |
| 236 | +## Benefits |
| 237 | + |
| 238 | +- **Thread-safe** -- no `static` buffers; all state lives on the stack |
| 239 | +- **No stack overflow risk** -- no large temporary byte arrays |
| 240 | +- **Single-pass encoding** -- data is written once, directly to the output bitstream |
| 241 | +- **Readable code** -- each function encodes only its own type's fields |
| 242 | +- **Proper function decomposition** -- the parent is a thin orchestrator; type-specific logic stays in type-specific functions |
| 243 | + |
| 244 | +## Supported ACN Patterns |
| 245 | + |
| 246 | +The following cross-boundary ACN patterns are handled by deferred patching: |
| 247 | + |
| 248 | +| Pattern | Description | |
| 249 | +|---------|-------------| |
| 250 | +| `CONTAINING` (OCTET STRING) | Size measured in bytes via `Acn_BitStream_DistanceInBytes` | |
| 251 | +| `CONTAINING` (BIT STRING) | Size measured in bits via `Acn_BitStream_DistanceInBits` | |
| 252 | +| Size determinant | Integer field determines array/string element count | |
| 253 | +| Presence (boolean) | 1-bit flag determines OPTIONAL field presence | |
| 254 | +| Presence (integer) | Integer value encodes presence condition | |
| 255 | +| CHOICE determinant | Enumerated field selects CHOICE alternative | |
| 256 | +| Cross-boundary references | Determinant in one type, data in another (passed via `AcnInsertedFieldRef*`) | |
| 257 | +| Shared determinants | Same determinant consumed by multiple fields (consistency-checked) | |
0 commit comments