As ppx work on a syntactic level there are limits to what you can do. For example, I don’t think it is useful to try to write a ppx that translates
let decode_header str =
let* () = [%guard str > 16] in
let ts = String.get_int64_le str 0 in
let id = String.get_uint16_le str 8 in
let ipv4 = String.get_int32_le str 10 in
let port = String.get_uint16_le str 14 in
Ok (ts, id, ipv4, port)
into a version that uses accessors without bounds checks as the ppx would need to track if str is shadowed (doable but annoying), the ppx would be limited in what expressions it can convert (e.g. what if we instead write let port = String.get_uint16_le str (off + 14) in) and finally it would be somewhat brittle to shadowing of stdlib functions (for example if another unrelated String module is in scope).
Instead I would look at using ppx for generating decode_header from some specification language. Maybe it could be something like:
let decode_header = [%decode ( int64_le, uint16_le, int32_le, uint16_le )]
which would use the sizes of the int types to compute the expected length and the offsets. This would be close to what ppx_cstruct does. An issue is you can end up generating a lot of code that you don’t use, and it can be somewhat inflexible (what if you want to read from an offset? and in other cases you always read from offset 0. If you add an optional offset argument you now introduce back some branching and a dead code path in some cases).
A ppx could be useful to make @dinosaure’s lavoisier.ml less painful to use by expanding e.g. [%peano 16] into Peano.(z s s s s s s s s s s s s s s s t) (I hope I got the count right). But I suspect this is maybe not the most painful part of lavoisier.ml.
Regarding stability I think ppxlib has greatly improved the stability of the ppx ecosystem although I don’t think it will completely safeguard a ppx from breaking between OCaml releases.