H.0 The Goal
Moving code between ISO C99 and Ocean Edition I should be an engineering task rather than a port to a different programming model. In both directions, the work is usually mechanical cleanup.
A C programmer should be able to read Ocean Edition I immediately. An Ocean Edition I programmer should be able to return to C99 without learning another paradigm. See syntax.md section 3.
H.1 Moving C99 Source into Ocean Edition I
H.1.1 What the compiler will tell you
A conforming implementation produces a diagnostic for each construct that needs changing, and shall offer a suggested rewrite wherever one exists. The diagnostics a migration typically produces:
function declaration does not specify parameters
implicit array-to-pointer conversion
implicit function-to-pointer conversion
variable-length array
obsolete storage specifier
extern declaration contains initializer
duplicate file-scope definition
noncanonical declaration spelling
noncanonical type spelling
array-syntax function parameter
old-style function definition
multi-character character constant
string literal assigned to a non-const character pointer
mixed signed and unsigned arithmetic conversion
noncanonical type spelling: _Bool
macro definition of a keyword
integer constant used as a null pointer
null constant assigned to an integer
H.1.2 The rewrite table
| Diagnostic | Rewrite |
|---|---|
| function declaration does not specify parameters | int f(); becomes int f(void); |
| implicit array-to-pointer conversion | f(a) becomes f(&a[0]) |
| implicit function-to-pointer conversion | f(g) becomes f(&g) |
| array-syntax function parameter | void f(int a[]) becomes void f(int *a, size_t n) |
| variable-length array | allocate, or use a fixed maximum |
| obsolete storage specifier | delete register or auto |
| extern declaration contains initializer | delete extern |
| duplicate file-scope definition | keep one definition; make the others extern |
| noncanonical declaration spelling | reorder to storage, qualifiers, type, declarator |
| noncanonical type spelling | unsigned long int becomes unsigned long |
| old-style function definition | move the parameter types into the parameter list |
| multi-character character constant | build the value with shifts |
string literal assigned to char * |
make the pointer const char *, or copy the literal |
| bare or extern inline | add static |
gets |
fgets with an explicit size |
noncanonical type spelling _Bool |
bool |
macro definition of bool, true, false, null, or NULL |
delete the definition; they are keywords |
| integer constant used as a null pointer | int *p = 0 becomes int *p = null |
p == 0 on a pointer |
p == null |
int n = NULL |
int n = 0 |
H.1.3 Order of work
A migration goes faster in this order, because each step reduces the noise in the next.
- Fix declarations: prototypes, canonical spellings, canonical order, storage specifiers.
- Fix definitions: remove tentative definitions and
externinitializers. - Fix
inline. - Fix explicit addressing: arrays and functions at call sites.
- Fix remaining diagnostics: literals, parameters, variable-length arrays.
- Review the warnings that are not errors, especially mixed signed and unsigned arithmetic.
Steps 1 through 3 are almost entirely automatable. Step 4 is mechanical but touches many lines. Step 6 is the only step that requires thought, and it is the step that finds real defects.
H.1.4 What you gain, quietly
After migration, several classes of defect become impossible or become visible:
- an object read before it is written now reads zero rather than garbage,
- signed overflow has a result rather than deleting the code around it,
- a shift with a bad count traps rather than producing anything,
- a comparison of a signed count with an unsigned size gives the arithmetically correct answer,
- an out-of-bounds constant index is a translation error,
- a format string that does not match its arguments is a translation error,
- a function that forgets to return traps rather than returning whatever was in the return register.
H.1.5 Adopting namespaces in a prefixed codebase
A codebase using a prefix convention is already namespaced by hand, and converting it is a rename.
For a module whose names all begin with net_:
- add
#namespace "net"at the top of each of its source files and headers, - remove the
net_prefix from each declaration in those files, - change each use from another module to
net::, - change
#includeto#importfor those headers.
The linkage names change from net_open to net__open, so the whole module shall be converted together, and any C code calling it shall be updated to the new spelling. A codebase that cannot change its exported names should keep the prefix and skip this facility, which costs nothing since namespaces are optional.
Check for the underscore restriction first. Clause 6.4.2.0 forbids __ in an exported name inside a namespace. A codebase already using net__internal_helper shall rename before converting. An implementation diagnoses this, so the check is a compile rather than an audit.
H.1.6 Code that will not migrate cleanly
Two categories need judgment rather than a rewrite.
Code that relies on type-based aliasing for speed. Ocean Edition I does not infer non-aliasing from types, so a loop that was fast because the compiler assumed float * and int * were distinct may slow down. The fix is restrict, which says the same thing where a reader can see it. See clause 6.7.3.3.
Code that uses variable-length arrays structurally. A function taking int m[rows][cols] has no direct Ocean Edition I spelling. The rewrite is a pointer plus explicit index arithmetic, or a structure carrying the extents, and it is a real change to the interface.
H.2 Moving Ocean Edition I Source into C99
Most Ocean Edition I source is already valid C99, because the edition adds almost no syntax. The namespace mechanism of clause 1.5.2 is the exception, and it lowers by renaming. The constructs needing a lowering:
| Ocean Edition I construct | C99 form |
|---|---|
&array[0] |
valid C99 already |
&function |
valid C99 already |
| canonical declarations | valid C99 already |
static inline in headers |
valid C99 already |
| defined evaluation order | name the intermediate values |
| default zero initialization | write = { 0 } or = 0 explicitly |
| wrapping signed arithmetic | use unsigned types, or compiler flags |
| trapping division and shifts | insert explicit checks |
| value-preserving mixed comparison | insert an explicit sign check |
const char[] string literals |
usually already correct |
namespace and :: |
rename each member to its linkage name |
#namespace "a" |
delete, and prefix each declaration with a__ |
#import |
#include, with an include guard added to the header |
bool, true, false keywords |
#include <stdbool.h> |
null keyword |
NULL, with #include <stddef.h> |
null_t |
void *, or C23 nullptr_t |
#pragma once |
an include guard |
Evaluation order.
foo(a(), b());
lowers to:
T0 _a = a();
T1 _b = b();
foo(_a, _b);
Zero initialization.
struct Buffer buffer;
lowers to:
struct Buffer buffer = { 0 };
Boolean keywords.
Source using bool, true, and false compiles as C99 once <stdbool.h> is included, which a translator can add unconditionally. The only visible difference is that sizeof(true) becomes sizeof(int), and a program that depends on that was doing something unusual.
Namespaces.
A namespace lowers by renaming. Each member with external linkage takes its linkage name, and each qualified use is rewritten to match:
#namespace "net"
int open(const char *host)
{
return 0;
}
becomes:
int net__open(const char *host)
{
return 0;
}
and net::open(&host[0]) becomes net__open(&host[0]).
A member with internal linkage keeps its name, since it has no linkage name and cannot collide across units. Where two static members of different namespaces share a name in one translation unit after lowering, the translator renames one.
#import lowers to #include. Since an imported header needs no guard under clause 6.10.11.4 and an included one does, the translator adds a guard to any header that lacks one.
The macro isolation of clause 6.10.11.2 does not survive the rewrite, because #include has no way to express it. A lowered header sees the including file's macros again. For a self-contained header this changes nothing, since such a header does not consult a macro it did not define or import. A header that does consult one is not mechanically lowerable, and shall be rewritten by hand or kept in this edition.
Null constants.
NULL compiles as C99 once <stddef.h> is included. null lowers to NULL. The distinct type is lost, so a C99 back-translation reintroduces the possibility of assigning a null constant to an integer, which the original program did not do.
Header protection.
#pragma once
lowers to an include guard using a name derived from the file path:
#ifndef OCEAN_GUARD_buffer_h
#define OCEAN_GUARD_buffer_h
...
#endif
Mixed comparison.
if (i < u)
lowers to:
if (i < 0 || (unsigned)i < u)
A translator performing these lowerings mechanically produces portable C99 from Ocean Edition I source, which makes the edition usable as a source language for targets whose only compiler is a C compiler.
H.3 Interoperation Without Migration
Migration is not always necessary. Ocean Edition I links with C directly.
H.3.1 Calling C from Ocean Edition I
Declare the C function with a complete prototype, include its header, and call it. Arguments are passed under the target ABI, which both editions share.
#include <stdio.h>
char line[256];
if (fgets(&line[0], sizeof(line), stdin) != NULL)
{
process(&line[0]);
}
The only adjustment is the explicit element pointer.
H.3.2 Calling Ocean Edition I from C
An Ocean Edition I function is an ordinary function in the target ABI. A C translation unit declares it and calls it. Where the function is a member of a namespace, the C declaration uses its linkage name, and clause H.3.3 gives the rule.
A header shared by both editions should be written so that it is valid in each. That means complete prototypes, no array-syntax parameters, canonical type spellings, and static inline rather than bare inline. Such a header is valid C99 and valid Ocean Edition I with no conditional compilation.
Four facilities need care in a shared header. A header included by ISO C source shall not use namespace, ::, or #import, since none exists in that edition; declare linkage names directly and let an Ocean Edition I file supply the namespaced view. Use an include guard rather than #pragma once, because only this edition requires the pragma. Include <stdbool.h> before writing bool, which is harmless in Ocean Edition I under clause 7.2.15 and necessary in C99. Write NULL rather than null and include <stddef.h>, since NULL is spelled the same in both and null exists only here.
H.3.3 Namespaces across the boundary
Calling Ocean Edition I from C. Declare the linkage name, which clause 6.9.6.4 fixes:
/* Ocean Edition I: net.c */
#namespace "net"
int open(const char *host)
{
...
}
/* ISO C: caller.c */
extern int net__open(const char *host);
int main(void)
{
return net__open("example.org");
}
The rule is mechanical. Join the namespace names and the identifier with two underscores.
Calling C from Ocean Edition I. A C entity is a member of the global namespace and needs no qualification:
#namespace "net"
#include <stdio.h>
int open(const char *host)
{
printf("opening %s\n", host); /* the global printf */
return 0;
}
Exporting an unqualified name. Declare it outside any namespace. In a file governed by #namespace, that means putting the declaration in a different file, since the directive governs the whole file. In a file using the definition form, it means putting the declaration outside the braces.
H.3.4 The semantic boundary
Semantics do not cross the call. An Ocean Edition I function evaluates its body under this specification, and a C function evaluates its body under ISO C99, whichever unit called which. See clause 4.6.2.
Practical consequences:
- a C function may leave an output parameter uninitialized, so an Ocean Edition I caller shall not assume the zero-initialization rule applies to storage a C function wrote,
- a C function may rely on signed overflow being undefined, and its compiler may have optimized accordingly,
- a C library
memcpymay not be overlap-safe, so an implementation shall provide its own overlap-safememcpyrather than forwarding to the host one.
An implementation should document how it satisfies the library guarantees of clause 7 when the host platform's C library does not.
H.3.5 System headers
System headers are written in C and are not under the program author's control. They frequently use array-syntax parameters and noncanonical spellings.
An implementation should provide a documented way to consume such a header under C99 rules while compiling the including unit as Ocean Edition I, checking declarations for compatibility at the boundary. See clause 6.10.9.
H.4 Mixed Codebases
A codebase can hold both editions indefinitely. A workable arrangement:
- new translation units are Ocean Edition I,
- existing translation units stay C99 until someone has a reason to touch them,
- shared headers are written in the common subset described in clause H.3.2,
- the build system selects the edition per translation unit and records the choice,
- a translation unit is never partly one edition and partly the other, under clause 4.6.1.
This is the arrangement the front end architecture of syntax.md section 58 was designed to support. Legacy semantics live in the C99 front end, edition semantics live in the Ocean Edition I front end, and the back end serves both.