Line data Source code
1 : #ifndef HEADER_fd_src_funk_fd_funkier_h
2 : #define HEADER_fd_src_funk_fd_funkier_h
3 :
4 : /* Funk is a hybrid of a database and version control system designed
5 : for ultra high performance blockchain applications.
6 :
7 : The data model is a flat table of records. A record is a xid/key-val
8 : pair and records are fast O(1) indexable by their xid/key. xid is
9 : short for "transaction id" and xids have a compile time fixed size
10 : (e.g. 16 bytes). keys also have a compile time fixed size (e.g.
11 : 40 bytes). Record values can vary in length from zero to a compile
12 : time maximum size. The xid of all zeros is reserved for the "root"
13 : transaction described below. Outside this, there are no
14 : restrictions on what a record xid, key or val can be. Individual
15 : records can be created, updated, and deleted arbitrarily. They are
16 : just binary data as far as funk is concerned.
17 :
18 : The maximum number of records is practically only limited by the size
19 : of the workspace memory backing it. At present, each record requires
20 : 128 bytes of metadata (this includes records that are published and
21 : records that are in the process of being updated). In other words,
22 : about 13 GiB record metadata per hundred million records. The
23 : maximum number of records that can be held by a funk instance is set
24 : when that it was created (given the persistent and relocatable
25 : properties described below though, it is straightforward to resize
26 : this).
27 :
28 : The transaction model is richer than what is found in a regular
29 : database. A transaction is a xid-"updates to parent transaction"
30 : pair and transactions are fast O(1) indexable by xid. There is no
31 : limitation on the number of updates in a transaction. Updates to the
32 : record value are represented as the complete value record to make it
33 : trivial to apply cryptographic operations like hashing to all updated
34 : values in a transaction with file I/O, operating system calls, memory
35 : data marshalling overhead, etc.
36 :
37 : Like records, the maximum number of transactions in preparation is
38 : practically only limited by the size of the workspace memory backing
39 : it. At present, a transaction requires 96 bytes of memory. As such,
40 : it is practical to track a large number of forks during an extended
41 : period of time of consensus failure in a block chain application
42 : without using much workspace memory at all. The maximum number of
43 : transactions that can be in preparation at any given time by a funk
44 : instance is set when that it was created (as before, given the
45 : persistent and relocatable properties described below, it is
46 : straightforward to resize this).
47 :
48 : That is, a transaction is a compact representation of the entire
49 : history of _all_ the database records up to that transaction. We can
50 : trace a transaction's ancestors back to the "root" give the complete
51 : history of all database records up to that transaction. The “root”
52 : transaction is the ancestor of all transactions. The transaction
53 : history is linear from the root transaction until the "last
54 : published" transaction and cannot be modified.
55 :
56 : To start "preparing" a new transaction, we pick the new transaction's
57 : xid (ideally unique among all transactions thus far) and fork off a
58 : "parent" transaction. This operation virtually clones all database
59 : records in the parent transaction, even if the parent itself has not
60 : yet been "published". Given the above, the parent transaction can be
61 : the last published transaction or another in-preparation transaction.
62 :
63 : Record inserts, reads, removes take place within the context
64 : of a transaction, effectively isolating them to a private view of the
65 : world. If a transaction is "cancelled", the changes to a record are
66 : harmlessly discarded. Records in a transaction that has children
67 : cannot be changed ("frozen").
68 :
69 : As such, it is not possible to modify the records in transactions
70 : strictly before the last published transaction. However, it is
71 : possible to modify the records of the last published transaction if
72 : there is no transactions in preparation. This is useful, for
73 : example, loading up a transaction from a checkpointed state on
74 : startup. A common idiom at start of a block though is to fork the
75 : potential transaction of that block from its parent (freezing its
76 : parent) and then fork a child of the potential transaction that will
77 : hold updates to the block that are incrementally "merged" into the
78 : potential transaction as block processing progresses.
79 :
80 : Critically, in-preparation transactions form a tree of dependent and
81 : competing histories. This model matches blockchains, where
82 : speculative work can proceed on several blocks at once long before
83 : the blocks are finalized. When a transaction is published, all its
84 : ancestors are also published, any competing histories are
85 : cancelled, leaving only a linear history up to the published
86 : transaction. There is no practical limitation on the complexity of
87 : this tree.
88 :
89 : Under the hood, the database state is stored in NUMA and TLB
90 : optimized shared memory (i.e. fd_wksp) such that various database
91 : operations can be used concurrently by multiple threads distributed
92 : arbitrarily over multiple processes zero copy.
93 :
94 : Database operations are at algorithmic minimums with reasonably high
95 : performance implementations. Most are fast O(1) time and all are
96 : small O(1) space (e.g. in complex transaction tree operations, there
97 : is no use of dynamic allocation to hold temporaries and no use of
98 : recursion to bound stack utilization at trivial levels). Further,
99 : there are no explicit operating system calls and, given a well
100 : optimized workspace (i.e. the wksp pages fit within a core's TLBs) no
101 : implicit operating system calls. Critical operations (e.g. those
102 : that actually might impact transaction history) are fortified against
103 : memory corruption (e.g. robust against DoS attack by corrupting
104 : transaction metadata to create loops in transaction trees or going
105 : out of bounds in memory). Outside of record values, all memory used
106 : is preallocated. And record values are O(1) lockfree concurrent
107 : allocated via fd_alloc using the same wksp as funk (the
108 : implementation is structured in layers that are straightforward to
109 : retarget for particular applications as might be necessary).
110 :
111 : The shared memory used by a funk instance is within a workspace such
112 : that it is also persistent and remotely inspectable. For example, a
113 : process attached to a funk instance can be terminated and a new
114 : process can resume exactly where the original process left off
115 : instantly (e.g. no file I/O). Or a real-time monitor could be
116 : visualizing the ongoing activity in a database non-invasively (e.g.
117 : forks in flight, records updated by forks, etc). Or an auxiliary
118 : process could be lazily and non-invasively writing all published
119 : records to permanent storage in the background in parallel with
120 : on-going operations.
121 :
122 : The records are further stored in the workspace memory relocatably.
123 : For example, workspace memory could just be committed to a persistent
124 : memory as is (or backed by NVMe or such directly), copied to a
125 : different host, and processes on the new host could resume (indeed,
126 : though it wouldn't be space efficient, the shared memory region is
127 : usable as is as an on-disk checkpoint file). Or the workspace could
128 : be resized and what not to handle large needs than when the database
129 : was initially created and it all "just works".
130 :
131 : Limited concurrent (multithreaded) access is supported. As a
132 : general rule, transaction level operations
133 : (e.g. fd_funkier_txn_cancel and fd_funkier_txn_publish) have to be
134 : single-threaded. In this case, no other access is allowed at the
135 : same time. Purely record level operations are thread safe and can
136 : be arbitrarily interleaved across multiple cpus. Specifically,
137 : these are:
138 : fd_funkier_rec_query_try
139 : fd_funkier_rec_query_test
140 : fd_funkier_rec_query_try_global
141 : fd_funkier_rec_prepare
142 : fd_funkier_rec_publish
143 : fd_funkier_rec_cancel
144 : fd_funkier_rec_remove
145 : */
146 :
147 : //#include "fd_funkier_base.h" /* Includes ../util/fd_util.h */
148 : //#include "fd_funkier_txn.h" /* Includes fd_funkier_base.h */
149 : //#include "fd_funkier_rec.h" /* Includes fd_funkier_txn.h */
150 : #include "fd_funkier_val.h" /* Includes fd_funkier_rec.h */
151 :
152 : /* FD_FUNKIER_ALIGN describe the alignment needed
153 : for a funk. ALIGN should be a positive integer power of 2.
154 : The footprint is dynamic depending on map sizes. */
155 :
156 0 : #define FD_FUNKIER_ALIGN (4096UL)
157 :
158 : /* The details of a fd_funkier_private are exposed here to facilitate
159 : inlining various operations. */
160 :
161 3 : #define FD_FUNKIER_MAGIC (0xf17eda2ce7fc2c02UL) /* firedancer funk version 2 */
162 :
163 : struct __attribute__((aligned(FD_FUNKIER_ALIGN))) fd_funkier_private {
164 :
165 : /* Metadata */
166 :
167 : ulong magic; /* ==FD_FUNKIER_MAGIC */
168 : ulong funk_gaddr; /* wksp gaddr of this in the backing wksp, non-zero gaddr */
169 : ulong wksp_tag; /* Tag to use for wksp allocations, positive */
170 : ulong seed; /* Seed for various hashing function used under the hood, arbitrary */
171 : ulong cycle_tag; /* Next cycle_tag to use, used internally for various data integrity checks */
172 :
173 : /* The funk transaction map stores the details about transactions
174 : in preparation and their relationships to each other. This is a
175 : fd_map_para/fd_pool_para and more details are given in fd_funkier_txn.h
176 :
177 : txn_max is the maximum number of transactions that can be in
178 : preparation. Due to the use of compressed map indices to reduce
179 : workspace memory footprint required, txn_max is at most
180 : FD_FUNKIER_TXN_IDX_NULL (currently ~4B). This should be more than
181 : ample for anticipated uses cases ... e.g. every single validator in
182 : a pool of tens of thousands Solana validator had its own fork and
183 : with no consensus ever being achieved, a funk with txn_max at the
184 : limits of a compressed index will be chug along for days to weeks
185 : before running out of indexing space. But if ever needing to
186 : support more, it is straightforward to change the code to not use
187 : index compression. Then, a funk (with a planet sized workspace
188 : backing it) would survive a similar scenario for millions of years.
189 : Presumably, if such a situation arose, in the weeks to eons while
190 : there was consensus, somebody would notice and care enough to
191 : intervene (if not it is probably irrelevant to the real world
192 : anyway).
193 :
194 : txn_map_gaddr is the wksp gaddr of the fd_funkier_txn_map_t used by
195 : this funk.
196 :
197 : child_{head,tail}_cidx are compressed txn map indices. After
198 : decompression, they give the txn map index of the {oldest,youngest}
199 : child of funk (i.e. an in-preparation transaction whose parent
200 : transaction id is last_publish). FD_FUNKIER_TXN_IDX_NULL indicates
201 : the funk is childless. Thus, if head/tail is FD_FUNKIER_TXN_IDX_NULL,
202 : tail/head will be too. funk is "frozen" if it has children.
203 :
204 : last_publish is the ID of the last published transaction. It will
205 : be the root transaction if no transactions have been published.
206 : Will be the root transaction immediately after construction. */
207 :
208 : ulong txn_max; /* In [0,FD_FUNKIER_TXN_IDX_NULL] */
209 : ulong txn_map_gaddr; /* Non-zero wksp gaddr with tag wksp_tag
210 : seed ==fd_funkier_txn_map_seed (txn_map)
211 : txn_max==fd_funkier_txn_map_key_max(txn_map) */
212 : ulong txn_pool_gaddr;
213 : ulong txn_ele_gaddr;
214 :
215 : uint child_head_cidx; /* After decompression, in [0,txn_max) or FD_FUNKIER_TXN_IDX_NULL, FD_FUNKIER_TXN_IDX_NULL if txn_max 0 */
216 : uint child_tail_cidx; /* " */
217 :
218 : /* Padding to FD_FUNKIER_TXN_XID_ALIGN here */
219 :
220 : fd_funkier_txn_xid_t root[1]; /* Always equal to the root transaction */
221 : fd_funkier_txn_xid_t last_publish[1]; /* Root transaction immediately after construction, not root thereafter */
222 :
223 : /* The funk record map stores the details about all the records in
224 : the funk, including all those in the last published transaction and
225 : all those getting updated in an in-preparation translation. This
226 : is a fd_map_para/fd_pool_para and more details are given in fd_funkier_rec.h
227 :
228 : rec_max is the maximum number of records that can exist in this
229 : funk.
230 :
231 : rec_map_gaddr is the wksp gaddr of the fd_funkier_rec_map_t used by
232 : this funk. */
233 :
234 : ulong rec_max;
235 : ulong rec_map_gaddr; /* Non-zero wksp gaddr with tag wksp_tag
236 : seed ==fd_funkier_rec_map_seed (rec_map)
237 : rec_max==fd_funkier_rec_map_key_max(rec_map) */
238 : ulong rec_pool_gaddr;
239 : ulong rec_ele_gaddr;
240 : ulong rec_head_idx; /* Record map index of the first record, FD_FUNKIER_REC_IDX_NULL if none (from oldest to youngest) */
241 : ulong rec_tail_idx; /* " last " */
242 :
243 : /* The funk alloc is used for allocating wksp resources for record
244 : values. This is a fd_alloc and more details are given in
245 : fd_funkier_val.h. Allocations from this allocator will be tagged with
246 : wksp_tag and operations on this allocator will use concurrency
247 : group 0.
248 :
249 : TODO: Consider letting user just pass a join of alloc (and maybe
250 : the cgroup_idx to give the funk), inferring the wksp, cgroup from
251 : that and allocating exclusively from that? */
252 :
253 : ulong alloc_gaddr; /* Non-zero wksp gaddr with tag wksp tag */
254 :
255 : /* Padding to FD_FUNKIER_ALIGN here */
256 : };
257 :
258 : FD_PROTOTYPES_BEGIN
259 :
260 : /* Constructors */
261 :
262 : /* fd_funkier_align return FD_FUNKIER_ALIGN. */
263 :
264 : FD_FN_CONST ulong
265 : fd_funkier_align( void );
266 :
267 : /* fd_funkier_footprint returns the size need for funk and all
268 : auxiliary data structures. Note that only record valus are
269 : allocated dynamically. */
270 :
271 : FD_FN_CONST ulong
272 : fd_funkier_footprint( ulong txn_max,
273 : ulong rec_max );
274 :
275 : /* fd_wksp_new formats an unused wksp allocation with the appropriate
276 : alignment and footprint as a funk. Caller is not joined on return.
277 : Returns shmem on success and NULL on failure (shmem NULL, shmem
278 : misaligned, zero wksp_tag, shmem is not backed by a wksp ... logs
279 : details). A workspace can be used by multiple funks concurrently.
280 : They will dynamically share the underlying workspace (along with any
281 : other non-funk usage) but will otherwise act as completely separate
282 : non-conflicting funks. To help with various diagnostics, garbage
283 : collection and what not, all allocations to the underlying wksp are
284 : tagged with the given tag (positive). Ideally, the tag used here
285 : should be distinct from all other tags used by this workspace but
286 : this is not required. */
287 :
288 : void *
289 : fd_funkier_new( void * shmem,
290 : ulong wksp_tag,
291 : ulong seed,
292 : ulong txn_max,
293 : ulong rec_max );
294 :
295 : /* fd_funkier_join joins the caller to a funk instance. shfunk points to
296 : the first byte of the memory region backing the funk in the caller's
297 : address space. Returns an opaque handle of the join on success
298 : (IMPORTANT! DO NOT ASSUME THIS IS A CAST OF SHFUNK) and NULL on
299 : failure (NULL shfunk, misaligned shfunk, shfunk is not backed by a
300 : wksp, bad magic, ... logs details). Every successful join should
301 : have a matching leave. The lifetime of the join is until the
302 : matching leave or the thread group is terminated (joins are local to
303 : a thread group). */
304 :
305 : fd_funkier_t *
306 : fd_funkier_join( void * shfunk );
307 :
308 : /* fd_funkier_leave leaves an existing join. Returns the underlying
309 : shfunk (IMPORTANT! DO NOT ASSUME THIS IS A CAST OF FUNK) on success
310 : and NULL on failure. Reasons for failure include funk is NULL (logs
311 : details). */
312 :
313 : void *
314 : fd_funkier_leave( fd_funkier_t * funk );
315 :
316 : /* fd_funkier_delete unformats a wksp allocation used as a funk
317 : (additionally frees all wksp allocations used by that funk). Assumes
318 : nobody is or will be joined to the funk. Returns shmem on success
319 : and NULL on failure (logs details). Reasons for failure include
320 : shfunk is NULL, misaligned shfunk, shfunk is not backed by a
321 : workspace, etc. */
322 :
323 : void *
324 : fd_funkier_delete( void * shfunk );
325 :
326 : /* Accessors */
327 :
328 : /* fd_funkier_wksp returns the local join to the wksp backing the funk.
329 : The lifetime of the returned pointer is at least as long as the
330 : lifetime of the local join. Assumes funk is a current local join. */
331 :
332 0 : FD_FN_PURE static inline fd_wksp_t * fd_funkier_wksp( fd_funkier_t * funk ) { return (fd_wksp_t *)(((ulong)funk) - funk->funk_gaddr); }
333 :
334 : /* fd_funkier_wksp_tag returns the workspace allocation tag used by the
335 : funk for its wksp allocations. Will be positive. Assumes funk is a
336 : current local join. */
337 :
338 0 : FD_FN_PURE static inline ulong fd_funkier_wksp_tag( fd_funkier_t * funk ) { return funk->wksp_tag; }
339 :
340 : /* fd_funkier_seed returns the hash seed used by the funk for various hash
341 : functions. Arbitrary value. Assumes funk is a current local join.
342 : TODO: consider renaming hash_seed? */
343 :
344 0 : FD_FN_PURE static inline ulong fd_funkier_seed( fd_funkier_t * funk ) { return funk->seed; }
345 :
346 : /* fd_funkier_txn_max returns maximum number of in-preparations the funk
347 : can support. Assumes funk is a current local join. Return in
348 : [0,FD_FUNKIER_TXN_IDX_NULL]. */
349 :
350 0 : FD_FN_PURE static inline ulong fd_funkier_txn_max( fd_funkier_t * funk ) { return funk->txn_max; }
351 :
352 : /* fd_funkier_txn_map returns the funk's transaction map join. This
353 : join can copied by value and is generally stored as a stack variable. */
354 :
355 : static inline fd_funkier_txn_map_t
356 : fd_funkier_txn_map( fd_funkier_t * funk, /* Assumes current local join */
357 0 : fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */
358 0 : fd_funkier_txn_map_t join;
359 0 : fd_funkier_txn_map_join(
360 0 : &join,
361 0 : fd_wksp_laddr_fast( wksp, funk->txn_map_gaddr ),
362 0 : fd_wksp_laddr_fast( wksp, funk->txn_ele_gaddr ),
363 0 : funk->txn_max );
364 0 : return join;
365 0 : }
366 :
367 : /* fd_funkier_txn_pool returns the funk's transaction pool join. This
368 : join can copied by value and is generally stored as a stack variable. */
369 :
370 : static inline fd_funkier_txn_pool_t
371 : fd_funkier_txn_pool( fd_funkier_t * funk, /* Assumes current local join */
372 0 : fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */
373 0 : fd_funkier_txn_pool_t join;
374 0 : fd_funkier_txn_pool_join(
375 0 : &join,
376 0 : fd_wksp_laddr_fast( wksp, funk->txn_pool_gaddr ),
377 0 : fd_wksp_laddr_fast( wksp, funk->txn_ele_gaddr ),
378 0 : funk->txn_max );
379 0 : return join;
380 0 : }
381 :
382 : /* fd_funkier_last_publish_child_{head,tail} returns a pointer in the
383 : caller's address space to {oldest,young} transaction child of root, NULL if
384 : funk is childless. All pointers are in the caller's address space.
385 : These are all a fast O(1) but not fortified against memory data
386 : corruption. */
387 :
388 : FD_FN_PURE static inline fd_funkier_txn_t *
389 : fd_funkier_last_publish_child_head( fd_funkier_t * funk,
390 0 : fd_funkier_txn_pool_t * pool ) {
391 0 : ulong idx = fd_funkier_txn_idx( funk->child_head_cidx );
392 0 : if( fd_funkier_txn_idx_is_null( idx ) ) return NULL; /* TODO: Consider branchless? */
393 0 : return pool->ele + idx;
394 0 : }
395 :
396 : FD_FN_PURE static inline fd_funkier_txn_t *
397 : fd_funkier_last_publish_child_tail( fd_funkier_t * funk,
398 0 : fd_funkier_txn_pool_t * pool ) {
399 0 : ulong idx = fd_funkier_txn_idx( funk->child_tail_cidx );
400 0 : if( fd_funkier_txn_idx_is_null( idx ) ) return NULL; /* TODO: Consider branchless? */
401 0 : return pool->ele + idx;
402 0 : }
403 :
404 : /* fd_funkier_root returns a pointer in the caller's address space to the
405 : transaction id of the root transaction. Assumes funk is a current
406 : local join. Lifetime of the returned pointer is the lifetime of the
407 : current local join. The value at this pointer will always be the
408 : root transaction id. */
409 :
410 0 : FD_FN_CONST static inline fd_funkier_txn_xid_t const * fd_funkier_root( fd_funkier_t * funk ) { return funk->root; }
411 :
412 : /* fd_funkier_last_publish returns a pointer in the caller's address space
413 : to transaction id of the last published transaction. Assumes funk is
414 : a current local join. Lifetime of the returned pointer is the
415 : lifetime of the current local join. The value at this pointer will
416 : be constant until the next transaction is published. */
417 :
418 0 : FD_FN_CONST static inline fd_funkier_txn_xid_t const * fd_funkier_last_publish( fd_funkier_t * funk ) { return funk->last_publish; }
419 :
420 : /* fd_funkier_is_frozen returns 1 if the records of the last published
421 : transaction are frozen (i.e. the funk has children) and 0 otherwise
422 : (i.e. the funk is childless). Assumes funk is a current local join. */
423 :
424 : FD_FN_PURE static inline int
425 0 : fd_funkier_last_publish_is_frozen( fd_funkier_t const * funk ) {
426 0 : return fd_funkier_txn_idx( funk->child_head_cidx )!=FD_FUNKIER_TXN_IDX_NULL;
427 0 : }
428 :
429 : /* fd_funkier_rec_max returns maximum number of records that can be held
430 : in the funk. This includes both records of the last published
431 : transaction and records for transactions that are in-flight. */
432 :
433 0 : FD_FN_PURE static inline ulong fd_funkier_rec_max( fd_funkier_t * funk ) { return funk->rec_max; }
434 :
435 : /* fd_funkier_rec_map returns the funk's record map join. This
436 : join can copied by value and is generally stored as a stack variable. */
437 :
438 : static inline fd_funkier_rec_map_t
439 : fd_funkier_rec_map( fd_funkier_t * funk, /* Assumes current local join */
440 0 : fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */
441 0 : fd_funkier_rec_map_t join;
442 0 : fd_funkier_rec_map_join(
443 0 : &join,
444 0 : fd_wksp_laddr_fast( wksp, funk->rec_map_gaddr ),
445 0 : fd_wksp_laddr_fast( wksp, funk->rec_ele_gaddr ),
446 0 : funk->rec_max );
447 0 : return join;
448 0 : }
449 :
450 : /* fd_funkier_rec_pool returns the funk's record pool join. This
451 : join can copied by value and is generally stored as a stack variable. */
452 :
453 : static inline fd_funkier_rec_pool_t
454 : fd_funkier_rec_pool( fd_funkier_t * funk, /* Assumes current local join */
455 0 : fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */
456 0 : fd_funkier_rec_pool_t join;
457 0 : fd_funkier_rec_pool_join(
458 0 : &join,
459 0 : fd_wksp_laddr_fast( wksp, funk->rec_pool_gaddr ),
460 0 : fd_wksp_laddr_fast( wksp, funk->rec_ele_gaddr ),
461 0 : funk->rec_max );
462 0 : return join;
463 0 : }
464 :
465 : /* fd_funkier_alloc returns a pointer in the caller's address space to
466 : the funk's allocator. */
467 :
468 : FD_FN_PURE static inline fd_alloc_t * /* Lifetime is that of the local join */
469 : fd_funkier_alloc( fd_funkier_t * funk, /* Assumes current local join */
470 0 : fd_wksp_t * wksp ) { /* Assumes wksp == fd_funkier_wksp( funk ) */
471 0 : return fd_alloc_join_cgroup_hint_set( (fd_alloc_t *)fd_wksp_laddr_fast( wksp, funk->alloc_gaddr ), fd_tile_idx() );
472 0 : }
473 :
474 : /* Misc */
475 :
476 : #ifdef FD_FUNKIER_HANDHOLDING
477 : /* fd_funkier_verify verifies the integrity of funk. Returns
478 : FD_FUNKIER_SUCCESS if funk appears to be intact and FD_FUNKIER_ERR_INVAL
479 : otherwise (logs details). Assumes funk is a current local join (NULL
480 : returns FD_FUNKIER_ERR_INVAL and logs details.) */
481 :
482 : int
483 : fd_funkier_verify( fd_funkier_t * funk );
484 : #endif
485 :
486 : FD_PROTOTYPES_END
487 :
488 : #endif /* HEADER_fd_src_funk_fd_funkier_h */
|