Skip to content

Commit c58a528

Browse files
OlivierNicolevouillonhhugo
authored
Runtime: implement Json output for Wasm (#1660)
Co-authored-by: Jérôme Vouillon <jerome.vouillon@gmail.com> Co-authored-by: Hugo Heuzard <hugo.heuzard@gmail.com>
1 parent 2f98d17 commit c58a528

File tree

5 files changed

+162
-15
lines changed

5 files changed

+162
-15
lines changed

lib/js_of_ocaml/js_of_ocaml_stubs.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ void caml_bytes_of_array () {
44
caml_fatal_error("Unimplemented Javascript primitive caml_bytes_of_array!");
55
}
66

7+
void caml_custom_identifier () {
8+
caml_fatal_error("Unimplemented Javascript primitive caml_custom_identifier!");
9+
}
10+
711
void caml_js_error_of_exception () {
812
caml_fatal_error("Unimplemented Javascript primitive caml_js_error_of_exception!");
913
}

lib/js_of_ocaml/json.ml

Lines changed: 114 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,93 @@
2020
open Js
2121
open! Import
2222

23+
(****)
24+
25+
(* The writing logic for basic types is copied from [lib/deriving_json]. *)
26+
27+
let write_string buffer s =
28+
Buffer.add_char buffer '"';
29+
for i = 0 to String.length s - 1 do
30+
match s.[i] with
31+
| '"' -> Buffer.add_string buffer {|\"|}
32+
| '\\' -> Buffer.add_string buffer {|\\|}
33+
| '\b' -> Buffer.add_string buffer {|\b|}
34+
| '\x0C' -> Buffer.add_string buffer {|\f|}
35+
| '\n' -> Buffer.add_string buffer {|\n|}
36+
| '\r' -> Buffer.add_string buffer {|\r|}
37+
| '\t' -> Buffer.add_string buffer {|\t|}
38+
| c when Poly.(c <= '\x1F') ->
39+
(* Other control characters are escaped. *)
40+
Printf.bprintf buffer {|\u%04X|} (int_of_char c)
41+
| c when Poly.(c < '\x80') -> Buffer.add_char buffer s.[i]
42+
| _c (* >= '\x80' *) ->
43+
(* Bytes greater than 127 are embedded in a UTF-8 sequence. *)
44+
Buffer.add_char buffer (Char.chr (0xC2 lor (Char.code s.[i] lsr 6)));
45+
Buffer.add_char buffer (Char.chr (0x80 lor (Char.code s.[i] land 0x3F)))
46+
done;
47+
Buffer.add_char buffer '"'
48+
49+
let write_float buffer f =
50+
(* "%.15g" can be (much) shorter; "%.17g" is round-trippable *)
51+
let s = Printf.sprintf "%.15g" f in
52+
if Poly.(float_of_string s = f)
53+
then Buffer.add_string buffer s
54+
else Printf.bprintf buffer "%.17g" f
55+
56+
let write_int64 buffer i =
57+
let mask16 = Int64.of_int 0xffff in
58+
let mask24 = Int64.of_int 0xffffff in
59+
Printf.bprintf
60+
buffer
61+
"[255,%Ld,%Ld,%Ld]"
62+
(Int64.logand i mask24)
63+
(Int64.logand (Int64.shift_right i 24) mask24)
64+
(Int64.logand (Int64.shift_right i 48) mask16)
65+
66+
external custom_identifier : Obj.t -> string = "caml_custom_identifier"
67+
68+
let rec write b v =
69+
if Obj.is_int v
70+
then Printf.bprintf b "%d" (Obj.obj v : int)
71+
else
72+
let t = Obj.tag v in
73+
if t <= Obj.last_non_constant_constructor_tag
74+
then (
75+
Printf.bprintf b "[%d" t;
76+
for i = 0 to Obj.size v - 1 do
77+
Buffer.add_char b ',';
78+
write b (Obj.field v i)
79+
done;
80+
Buffer.add_char b ']')
81+
else if t = Obj.string_tag
82+
then write_string b (Obj.obj v : string)
83+
else if t = Obj.double_tag
84+
then write_float b (Obj.obj v : float)
85+
else if t = Obj.double_array_tag
86+
then (
87+
Printf.bprintf b "[%d" t;
88+
for i = 0 to Obj.size v - 1 do
89+
Buffer.add_char b ',';
90+
write_float b (Obj.double_field v i)
91+
done;
92+
Buffer.add_char b ']')
93+
else if t = Obj.custom_tag
94+
then
95+
match custom_identifier v with
96+
| "_i" -> Printf.bprintf b "%ld" (Obj.obj v : int32)
97+
| "_j" ->
98+
let i : int64 = Obj.obj v in
99+
write_int64 b i
100+
| id -> failwith (Printf.sprintf "Json.output: unsupported custom value %s " id)
101+
else failwith (Printf.sprintf "Json.output: unsupported tag %d " t)
102+
103+
let to_json v =
104+
let buf = Buffer.create 50 in
105+
write buf v;
106+
Buffer.contents buf
107+
108+
(****)
109+
23110
class type json = object
24111
method parse : 'a. js_string t -> 'a meth
25112

@@ -51,13 +138,22 @@ let input_reviver =
51138
in
52139
wrap_meth_callback reviver
53140

54-
let unsafe_input s = json##parse_ s input_reviver
141+
let unsafe_input s =
142+
match Sys.backend_type with
143+
| Other "wasm_of_ocaml" ->
144+
(* https://github.com/ocsigen/js_of_ocaml/pull/1660#discussion_r1731099372
145+
The encoding of OCaml values is ambiguous since both integers and floats
146+
are mapped to numbers *)
147+
failwith "Json.unsafe_input: not implemented in the Wasm backend"
148+
| _ -> json##parse_ s input_reviver
55149

56150
class type obj = object
57151
method constructor : 'a. 'a constr Js.readonly_prop
58152
end
59153

60154
let mlInt64_constr =
155+
Js.Unsafe.pure_expr
156+
@@ fun () ->
61157
let dummy_int64 = 1L in
62158
let dummy_obj : obj t = Obj.magic dummy_int64 in
63159
dummy_obj##.constructor
@@ -71,4 +167,20 @@ let output_reviver _key (value : Unsafe.any) : Obj.t =
71167
Obj.repr (array [| 255; value##.lo; value##.mi; value##.hi |])
72168
else Obj.repr value
73169

74-
let output obj = json##stringify_ obj (Js.wrap_callback output_reviver)
170+
let use_native_stringify_ =
171+
ref
172+
(match Sys.backend_type with
173+
| Other "js_of_ocaml" -> true
174+
| Native | Bytecode | Other _ -> false)
175+
176+
let use_native_stringify () = !use_native_stringify_
177+
178+
let set_use_native_stringify b = use_native_stringify_ := b
179+
180+
let output_ x = to_json (Obj.repr x)
181+
182+
let output obj =
183+
match Sys.backend_type with
184+
| Other "js_of_ocaml" when use_native_stringify () ->
185+
json##stringify_ obj (Js.wrap_callback output_reviver)
186+
| _ -> Js.string (output_ obj)

lib/js_of_ocaml/json.mli

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,3 +25,16 @@ val output : 'a -> Js.js_string Js.t
2525
val unsafe_input : Js.js_string Js.t -> 'a
2626
(** Unmarshal a string in JSON format as an OCaml value (unsafe but
2727
fast !). *)
28+
29+
(**/**)
30+
31+
val output_ : 'a -> string
32+
33+
val set_use_native_stringify : bool -> unit
34+
(** Only affects js_of_ocaml. Whether to use native Javascript [stringify] to
35+
turn a value into JSON in {!val:output}. Otherwise, fall back to the slower
36+
method used by other backends, such as wasm_of_ocaml. *)
37+
38+
val use_native_stringify : unit -> bool
39+
(** Whether js_of_ocaml is using [stringify] in {!val:output}. See
40+
{!val:set_use_native_stringify}. *)

lib/tests/test_json.ml

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,30 +22,42 @@ open Js_of_ocaml
2222

2323
let round_trip x =
2424
let s = Json.output x in
25-
Printf.printf "%s\n" (Js.to_bytestring s);
26-
let y = Json.unsafe_input s in
27-
Printf.printf "%b\n" (x = y)
25+
let s1 = Js.to_bytestring s in
26+
let s2 =
27+
let old = Json.use_native_stringify () in
28+
Json.set_use_native_stringify false;
29+
let s = Json.output x in
30+
Json.set_use_native_stringify old;
31+
Js.to_bytestring s
32+
in
33+
Printf.printf "%s\n" s1;
34+
if s1 <> s2 then Printf.printf "Json.output mismatch: %s vs %s\n" s1 s2;
35+
(* Other direction of the round-trip (unmarshalling from JSON) is only
36+
available with js_of_ocaml *)
37+
match Sys.backend_type with
38+
| Other "js_of_ocaml" when Json.use_native_stringify () ->
39+
let y = Json.unsafe_input s in
40+
if not (x = y) then Printf.printf "not invariant by round-trip\n"
41+
| _ -> ()
2842

2943
let%expect_test _ =
3044
round_trip 123L;
3145
[%expect {|
32-
[255,123,0,0]
33-
true |}];
46+
[255,123,0,0] |}];
3447
round_trip "asd";
3548
[%expect {|
36-
"asd"
37-
true |}];
49+
"asd" |}];
3850
round_trip "\000\255\254";
39-
[%expect {|
40-
"\u0000ÿþ"
41-
true |}];
51+
[%expect {| "\u0000ÿþ" |}];
4252
round_trip (2, 3);
4353
round_trip (2., 3.);
4454
round_trip (2.2, 3.3);
4555
[%expect {|
4656
[0,2,3]
47-
true
4857
[0,2,3]
49-
true
5058
[0,2.2,3.3]
51-
true |}]
59+
|}];
60+
round_trip [| 1.; 2.; 3. |];
61+
[%expect {| [254,1,2,3] |}];
62+
round_trip 2n;
63+
[%expect {| 2 |}]

runtime/obj.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,3 +214,9 @@ function caml_is_continuation_tag(t) {
214214
function caml_is_continuation_tag(t) {
215215
return (t == 245) ? 1 : 0;
216216
}
217+
218+
//Provides: caml_custom_identifier
219+
//Requires: caml_string_of_jsstring
220+
function caml_custom_identifier (o) {
221+
return caml_string_of_jsstring(o.caml_custom);
222+
}

0 commit comments

Comments
 (0)