There's a trick with existential types, as used in e.g. Haskell's ST monad. It uses the fact that an existentially-quantified type variable can't escape its scope, so if your channel type and results that depend on it are parametrised by an existential type variable, the corresponding values can't escape the scope of the callback either.
Something like:
module ST : sig
type ('a, 's) t
include Monad.S2 with type ('a, 's) t := ('a, 's) t
type 's chan
type 'a f = { f : 's . 's chan -> ('a, 's) t }
val with_file : string -> f:'a f -> 'a
val input_line : 's chan -> (string option, 's) t
end = struct
module T = struct
type ('a, 's) t = 'a
let return x = x
let bind x f = f x
let map x ~f = f x
end
include T
include Monad.Make2(T)
type 's chan = In_channel.t
type 'a f = { f : 's . 's chan -> ('a, 's) t }
let with_file fp ~f:{ f } = In_channel.with_file fp ~f
let input_line c = In_channel.input_line c
end
;;
match ST.with_file "
safe.ml" ~f:{ ST.f = fun c -> ST.input_line c } with
| None -> print_endline "None"
| Some line -> print_endline line