declare-c generates the wrapper, the typedefs and the prototype from one declaration, so they cannot disagree with each other. What nothing checked was whether the declaration matched the library — BUILT.md records that as trusted rather than guaranteed, because no header was ever read. This reads one. clang is asked for a JSON AST dump of the header and shelled out to, not linked: -Xclang -ast-dump=json is the same binary on PATH that every build already runs, which is plan.org's "Why LLVM IR as text" applied a second time. Zig's old @cImport linked clang as a library and that is precisely the dependency plan.org rejected. cjson.ml is enough JSON to read the dump and no more, so this adds no opam package to parse it. What comes out of the header is signatures and nothing else — not structs, not enums, not macros. The bound on how much is imported is the package's own defstructs: a function whose signature mentions a struct the package has not described is refused with that reason, so vendor/raylib describing thirteen structs is what makes the import thirteen structs wide. Keeping the layouts hand-written is also what makes checking them against the header's records worth doing — a _Static_assert was rejected in BUILT.md as circular, and this is not, because the two sides have different authors. Refusals are demotions, taken from Zig's translator: it never drops a declaration it cannot handle, it binds the name to a @compileError carrying the reason so the failure lands at the use site. Load.refuse_hidden is already that mechanism. So a returned char * does not kill the header — it makes one name unavailable, with the reason attached. flan import-c prints what it would produce, what it refused, how the package's defstructs compare with the header's records, and how the hand-written declare-c lines compare with the header's signatures. Against raylib 5.5, the version whose .so vendor/raylib/link names: all 16 defstructs and all 172 hand-written declare-c agree exactly. Against the 5.1-dev header installed in /usr/local it reports ten differences, nine functions that version does not have and one that gained a parameter — so the check has teeth and the clean run is not a vacuous one.
175 lines
5.9 KiB
OCaml
175 lines
5.9 KiB
OCaml
(** Just enough JSON to read clang's AST dump.
|
|
|
|
Not a general JSON library and not a dependency. The compiler's build
|
|
inputs are a [clang] on PATH and nothing else — that is plan.org's "Why
|
|
LLVM IR as text" applied a second time — so reading clang's
|
|
[-ast-dump=json] must not drag in an opam package to parse it. What the
|
|
dump actually contains is a narrow subset: objects, arrays, strings,
|
|
integers, [true]/[false]/[null]. No floats appear in a declaration dump,
|
|
but one is accepted anyway rather than being a lurking parse error.
|
|
|
|
The reader is strict about structure and lax about what it keeps: a dump of
|
|
raylib.h is 1.8 MB and roughly fifty thousand objects, almost all of it
|
|
source ranges nobody asks for. Parsing it whole and then selecting is still
|
|
well under the cost of the [clang] process that produced it, so there is no
|
|
streaming filter here and no reason for one. *)
|
|
|
|
type t =
|
|
| Null
|
|
| Bool of bool
|
|
| Num of float
|
|
| Str of string
|
|
| Arr of t list
|
|
| Obj of (string * t) list
|
|
|
|
exception Bad of string
|
|
|
|
let bad fmt = Printf.ksprintf (fun m -> raise (Bad m)) fmt
|
|
|
|
let parse (s : string) : t =
|
|
let n = String.length s in
|
|
let i = ref 0 in
|
|
let peek () = if !i < n then s.[!i] else '\000' in
|
|
let rec skip_ws () =
|
|
if !i < n then
|
|
match s.[!i] with
|
|
| ' ' | '\t' | '\n' | '\r' -> incr i; skip_ws ()
|
|
| _ -> ()
|
|
in
|
|
let expect c =
|
|
if !i >= n || s.[!i] <> c then
|
|
bad "expected %c at byte %d" c !i
|
|
else incr i
|
|
in
|
|
let lit word v =
|
|
let l = String.length word in
|
|
if !i + l <= n && String.sub s !i l = word then (i := !i + l; v)
|
|
else bad "bad literal at byte %d" !i
|
|
in
|
|
(* Strings are the hot path — every node has several — so the common case of
|
|
no escape at all is copied out in one [String.sub] rather than a character
|
|
at a time through a Buffer. *)
|
|
let string_ () =
|
|
expect '"';
|
|
let start = !i in
|
|
let rec scan plain =
|
|
if !i >= n then bad "unterminated string at byte %d" start
|
|
else
|
|
match s.[!i] with
|
|
| '"' -> plain
|
|
| '\\' -> i := !i + 2; scan false
|
|
| _ -> incr i; scan plain
|
|
in
|
|
let plain = scan true in
|
|
if plain then begin
|
|
let r = String.sub s start (!i - start) in
|
|
incr i; r
|
|
end
|
|
else begin
|
|
let b = Buffer.create (!i - start) in
|
|
let j = ref start in
|
|
while !j < !i do
|
|
(match s.[!j] with
|
|
| '\\' ->
|
|
incr j;
|
|
(match s.[!j] with
|
|
| 'n' -> Buffer.add_char b '\n'
|
|
| 't' -> Buffer.add_char b '\t'
|
|
| 'r' -> Buffer.add_char b '\r'
|
|
| 'b' -> Buffer.add_char b '\b'
|
|
| 'f' -> Buffer.add_char b '\012'
|
|
| '/' -> Buffer.add_char b '/'
|
|
| '"' -> Buffer.add_char b '"'
|
|
| '\\' -> Buffer.add_char b '\\'
|
|
| 'u' ->
|
|
(* clang escapes a non-ASCII identifier or a comment this way.
|
|
Encoded as UTF-8; a surrogate pair is not joined, which is
|
|
acceptable because nothing this reads is ever a name Flan
|
|
could use anyway. *)
|
|
let hex = String.sub s (!j + 1) 4 in
|
|
j := !j + 4;
|
|
let c = int_of_string ("0x" ^ hex) in
|
|
if c < 0x80 then Buffer.add_char b (Char.chr c)
|
|
else if c < 0x800 then begin
|
|
Buffer.add_char b (Char.chr (0xC0 lor (c lsr 6)));
|
|
Buffer.add_char b (Char.chr (0x80 lor (c land 0x3F)))
|
|
end
|
|
else begin
|
|
Buffer.add_char b (Char.chr (0xE0 lor (c lsr 12)));
|
|
Buffer.add_char b (Char.chr (0x80 lor ((c lsr 6) land 0x3F)));
|
|
Buffer.add_char b (Char.chr (0x80 lor (c land 0x3F)))
|
|
end
|
|
| c -> bad "unknown escape \\%c at byte %d" c !j)
|
|
| c -> Buffer.add_char b c);
|
|
incr j
|
|
done;
|
|
incr i;
|
|
Buffer.contents b
|
|
end
|
|
in
|
|
let number () =
|
|
let start = !i in
|
|
if peek () = '-' then incr i;
|
|
let digits () = while !i < n && s.[!i] >= '0' && s.[!i] <= '9' do incr i done in
|
|
digits ();
|
|
if peek () = '.' then (incr i; digits ());
|
|
if peek () = 'e' || peek () = 'E' then begin
|
|
incr i;
|
|
if peek () = '+' || peek () = '-' then incr i;
|
|
digits ()
|
|
end;
|
|
if !i = start then bad "expected a number at byte %d" start;
|
|
Num (float_of_string (String.sub s start (!i - start)))
|
|
in
|
|
let rec value () =
|
|
skip_ws ();
|
|
match peek () with
|
|
| '{' ->
|
|
incr i; skip_ws ();
|
|
if peek () = '}' then (incr i; Obj [])
|
|
else begin
|
|
let acc = ref [] in
|
|
let rec members () =
|
|
skip_ws ();
|
|
let k = string_ () in
|
|
skip_ws (); expect ':';
|
|
let v = value () in
|
|
acc := (k, v) :: !acc;
|
|
skip_ws ();
|
|
if peek () = ',' then (incr i; members ()) else expect '}'
|
|
in
|
|
members ();
|
|
Obj (List.rev !acc)
|
|
end
|
|
| '[' ->
|
|
incr i; skip_ws ();
|
|
if peek () = ']' then (incr i; Arr [])
|
|
else begin
|
|
let acc = ref [] in
|
|
let rec items () =
|
|
let v = value () in
|
|
acc := v :: !acc;
|
|
skip_ws ();
|
|
if peek () = ',' then (incr i; items ()) else expect ']'
|
|
in
|
|
items ();
|
|
Arr (List.rev !acc)
|
|
end
|
|
| '"' -> Str (string_ ())
|
|
| 't' -> lit "true" (Bool true)
|
|
| 'f' -> lit "false" (Bool false)
|
|
| 'n' -> lit "null" Null
|
|
| _ -> number ()
|
|
in
|
|
let v = value () in
|
|
skip_ws ();
|
|
if !i <> n then bad "trailing bytes at %d" !i;
|
|
v
|
|
|
|
(* ── Getters ───────────────────────────────────────────────────────── *)
|
|
|
|
let mem k = function Obj kvs -> List.assoc_opt k kvs | _ -> None
|
|
let str k j = match mem k j with Some (Str s) -> Some s | _ -> None
|
|
let bool k j = match mem k j with Some (Bool b) -> b | _ -> false
|
|
let arr k j = match mem k j with Some (Arr l) -> l | _ -> []
|