Skip to content

Commit 18969c8

Browse files
committed
documentation
1 parent 84055cf commit 18969c8

File tree

2 files changed

+259
-0
lines changed

2 files changed

+259
-0
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,8 @@ Asn1.userprefs
409409
/SPARK_RTL/adaasn1rtl.o
410410
/SPARK_RTL/spark_io_05.ali
411411
/SPARK_RTL/spark_io_05.o
412+
/.claude
413+
/q
412414
/mantis/0000411/out_c
413415
/mantis/0000411/out_ada
414416
/mantis/0000444 - Custom templates/custom_icd_acn.html

Docs/ACN-Deferred-Patching.md

Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
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

Comments
 (0)