mirror of
https://github.com/ocaml-multicore/eio.git
synced 2025-12-08 00:02:56 -05:00
Move Eio_null to Eio_mock.Backend
It can be useful for tests to be able to run an event loop without having to depend on eio_main. Also, replace the inefficient list-based queue with a proper Lf_queue.
This commit is contained in:
parent
475c03ed26
commit
c4d19d5495
@ -1170,7 +1170,7 @@ This may be useful during the process of porting existing code to Eio.
|
|||||||
|
|
||||||
- [lib_eio/eio.mli](lib_eio/eio.mli) documents Eio's public API.
|
- [lib_eio/eio.mli](lib_eio/eio.mli) documents Eio's public API.
|
||||||
- [doc/rationale.md](doc/rationale.md) describes some of Eio's design tradeoffs in more detail.
|
- [doc/rationale.md](doc/rationale.md) describes some of Eio's design tradeoffs in more detail.
|
||||||
- [doc/eio_null.md](doc/eio_null.md) is a skeleton Eio backend with no actual IO.
|
- [lib_eio/mock/backend.ml](lib_eio/mock/backend.ml) is a skeleton Eio backend with no actual IO.
|
||||||
|
|
||||||
Some background about the effects system can be found in:
|
Some background about the effects system can be found in:
|
||||||
|
|
||||||
|
|||||||
2
doc/dune
2
doc/dune
@ -1,4 +1,4 @@
|
|||||||
(mdx
|
(mdx
|
||||||
(package eio_main)
|
(package eio_main)
|
||||||
(packages eio_main)
|
(packages eio_main)
|
||||||
(files multicore.md eio_null.md))
|
(files multicore.md))
|
||||||
|
|||||||
104
doc/eio_null.md
104
doc/eio_null.md
@ -1,104 +0,0 @@
|
|||||||
```ocaml
|
|
||||||
# #require "eio.utils";;
|
|
||||||
```
|
|
||||||
|
|
||||||
# A dummy Eio backend with no actual effects
|
|
||||||
|
|
||||||
This is very inefficient and not thread-safe, but it demonstrates the idea.
|
|
||||||
A real backend would typically pass `main` some way to interact with it, like the other backends do.
|
|
||||||
|
|
||||||
```ocaml
|
|
||||||
open Eio.Std
|
|
||||||
|
|
||||||
(* An Eio backend with no actual IO *)
|
|
||||||
module Eio_null = struct
|
|
||||||
module Fiber_context = Eio.Private.Fiber_context
|
|
||||||
module Effect = Eio.Private.Effect (* For compatibility with 4.12+domains *)
|
|
||||||
|
|
||||||
(* The scheduler could just return [unit], but this is clearer. *)
|
|
||||||
type exit = Exit_scheduler
|
|
||||||
|
|
||||||
type t = {
|
|
||||||
(* Suspended fibers waiting to run again.
|
|
||||||
A real system would probably use [Eio_utils.Lf_queue]. *)
|
|
||||||
mutable run_q : (unit -> exit) list;
|
|
||||||
}
|
|
||||||
|
|
||||||
(* Resume the next runnable fiber, if any. *)
|
|
||||||
let schedule t : exit =
|
|
||||||
match t.run_q with
|
|
||||||
| f :: fs -> t.run_q <- fs; f ()
|
|
||||||
| [] -> Exit_scheduler (* Finished (or deadlocked) *)
|
|
||||||
|
|
||||||
(* Run [main] in an Eio main loop. *)
|
|
||||||
let run main =
|
|
||||||
let t = { run_q = [] } in
|
|
||||||
let rec fork ~new_fiber:fiber fn =
|
|
||||||
(* Create a new fiber and run [fn] in it. *)
|
|
||||||
Effect.Deep.match_with fn ()
|
|
||||||
{ retc = (fun () -> Fiber_context.destroy fiber; schedule t);
|
|
||||||
exnc = (fun ex -> Fiber_context.destroy fiber; raise ex);
|
|
||||||
effc = fun (type a) (e : a Effect.t) : ((a, exit) Effect.Deep.continuation -> exit) option ->
|
|
||||||
match e with
|
|
||||||
| Eio.Private.Effects.Suspend f -> Some (fun k ->
|
|
||||||
(* Ask [f] to register whatever callbacks are needed to resume the fiber.
|
|
||||||
e.g. it might register a callback with a promise, for when that's resolved. *)
|
|
||||||
f fiber (function
|
|
||||||
(* The fiber is ready to run again. Add it to the queue. *)
|
|
||||||
| Ok v -> t.run_q <- t.run_q @ [fun () -> Effect.Deep.continue k v]
|
|
||||||
| Error ex -> t.run_q <- t.run_q @ [fun () -> Effect.Deep.discontinue k ex]
|
|
||||||
);
|
|
||||||
(* Switch to the next runnable fiber while this one's blocked. *)
|
|
||||||
schedule t
|
|
||||||
)
|
|
||||||
| Eio.Private.Effects.Fork (new_fiber, f) -> Some (fun k ->
|
|
||||||
(* Arrange for the forking fiber to run immediately after the new one. *)
|
|
||||||
t.run_q <- Effect.Deep.continue k :: t.run_q;
|
|
||||||
(* Create and run the new fiber (using fiber context [new_fiber]). *)
|
|
||||||
fork ~new_fiber f
|
|
||||||
)
|
|
||||||
| Eio.Private.Effects.Get_context -> Some (fun k ->
|
|
||||||
Effect.Deep.continue k fiber
|
|
||||||
)
|
|
||||||
| Eio.Private.Effects.Trace -> Some (fun k ->
|
|
||||||
Effect.Deep.continue k Eio.Private.default_traceln
|
|
||||||
)
|
|
||||||
| _ -> None
|
|
||||||
}
|
|
||||||
in
|
|
||||||
let new_fiber = Fiber_context.make_root () in
|
|
||||||
let Exit_scheduler = fork ~new_fiber main in
|
|
||||||
()
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
It supports forking, tracing, suspending and cancellation:
|
|
||||||
|
|
||||||
```ocaml
|
|
||||||
# Eio_null.run @@ fun () ->
|
|
||||||
let s = Eio.Stream.create 1 in
|
|
||||||
try
|
|
||||||
Fiber.both
|
|
||||||
(fun () ->
|
|
||||||
for x = 1 to 3 do
|
|
||||||
traceln "Sending %d" x;
|
|
||||||
Eio.Stream.add s x
|
|
||||||
done;
|
|
||||||
raise Exit
|
|
||||||
)
|
|
||||||
(fun () ->
|
|
||||||
while true do
|
|
||||||
traceln "Got %d" (Eio.Stream.take s)
|
|
||||||
done
|
|
||||||
)
|
|
||||||
with Exit ->
|
|
||||||
traceln "Finished!";;
|
|
||||||
+Sending 1
|
|
||||||
+Sending 2
|
|
||||||
+Got 1
|
|
||||||
+Got 2
|
|
||||||
+Sending 3
|
|
||||||
+Got 3
|
|
||||||
+Finished!
|
|
||||||
- : unit = ()
|
|
||||||
```
|
|
||||||
60
lib_eio/mock/backend.ml
Normal file
60
lib_eio/mock/backend.ml
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
module Fiber_context = Eio.Private.Fiber_context
|
||||||
|
module Effect = Eio.Private.Effect (* For compatibility with 4.12+domains *)
|
||||||
|
module Lf_queue = Eio_utils.Lf_queue
|
||||||
|
|
||||||
|
(* The scheduler could just return [unit], but this is clearer. *)
|
||||||
|
type exit = Exit_scheduler
|
||||||
|
|
||||||
|
type t = {
|
||||||
|
(* Suspended fibers waiting to run again.
|
||||||
|
[Lf_queue] is like [Stdlib.Queue], but is thread-safe (lock-free) and
|
||||||
|
allows pushing items to the head too, which we need. *)
|
||||||
|
mutable run_q : (unit -> exit) Lf_queue.t;
|
||||||
|
}
|
||||||
|
|
||||||
|
(* Resume the next runnable fiber, if any. *)
|
||||||
|
let schedule t : exit =
|
||||||
|
match Lf_queue.pop t.run_q with
|
||||||
|
| Some f -> f ()
|
||||||
|
| None -> Exit_scheduler (* Finished (or deadlocked) *)
|
||||||
|
|
||||||
|
(* Run [main] in an Eio main loop. *)
|
||||||
|
let run main =
|
||||||
|
let t = { run_q = Lf_queue.create () } in
|
||||||
|
let rec fork ~new_fiber:fiber fn =
|
||||||
|
(* Create a new fiber and run [fn] in it. *)
|
||||||
|
Effect.Deep.match_with fn ()
|
||||||
|
{ retc = (fun () -> Fiber_context.destroy fiber; schedule t);
|
||||||
|
exnc = (fun ex -> Fiber_context.destroy fiber; raise ex);
|
||||||
|
effc = fun (type a) (e : a Effect.t) : ((a, exit) Effect.Deep.continuation -> exit) option ->
|
||||||
|
match e with
|
||||||
|
| Eio.Private.Effects.Suspend f -> Some (fun k ->
|
||||||
|
(* Ask [f] to register whatever callbacks are needed to resume the fiber.
|
||||||
|
e.g. it might register a callback with a promise, for when that's resolved. *)
|
||||||
|
f fiber (function
|
||||||
|
(* The fiber is ready to run again. Add it to the queue. *)
|
||||||
|
| Ok v -> Lf_queue.push t.run_q (fun () -> Effect.Deep.continue k v)
|
||||||
|
| Error ex -> Lf_queue.push t.run_q (fun () -> Effect.Deep.discontinue k ex)
|
||||||
|
);
|
||||||
|
(* Switch to the next runnable fiber while this one's blocked. *)
|
||||||
|
schedule t
|
||||||
|
)
|
||||||
|
| Eio.Private.Effects.Fork (new_fiber, f) -> Some (fun k ->
|
||||||
|
(* Arrange for the forking fiber to run immediately after the new one. *)
|
||||||
|
Lf_queue.push_head t.run_q (Effect.Deep.continue k);
|
||||||
|
(* Create and run the new fiber (using fiber context [new_fiber]). *)
|
||||||
|
fork ~new_fiber f
|
||||||
|
)
|
||||||
|
| Eio.Private.Effects.Get_context -> Some (fun k ->
|
||||||
|
Effect.Deep.continue k fiber
|
||||||
|
)
|
||||||
|
| Eio.Private.Effects.Trace -> Some (fun k ->
|
||||||
|
Effect.Deep.continue k Eio.Private.default_traceln
|
||||||
|
)
|
||||||
|
| _ -> None
|
||||||
|
}
|
||||||
|
in
|
||||||
|
let new_fiber = Fiber_context.make_root () in
|
||||||
|
let result = ref None in
|
||||||
|
let Exit_scheduler = fork ~new_fiber (fun () -> result := Some (main ())) in
|
||||||
|
Option.get !result
|
||||||
4
lib_eio/mock/backend.mli
Normal file
4
lib_eio/mock/backend.mli
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
(** A dummy Eio backend with no actual IO. *)
|
||||||
|
|
||||||
|
val run : (unit -> 'a) -> 'a
|
||||||
|
(** [run fn] runs an event loop and then calls [fn env] within it. *)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
(library
|
(library
|
||||||
(name eio_mock)
|
(name eio_mock)
|
||||||
(public_name eio.mock)
|
(public_name eio.mock)
|
||||||
(libraries eio))
|
(libraries eio eio.utils))
|
||||||
|
|||||||
@ -2,3 +2,4 @@ module Action = Action
|
|||||||
module Handler = Handler
|
module Handler = Handler
|
||||||
module Flow = Flow
|
module Flow = Flow
|
||||||
module Net = Net
|
module Net = Net
|
||||||
|
module Backend = Backend
|
||||||
|
|||||||
@ -80,7 +80,7 @@ module Handler : sig
|
|||||||
(** [run_default_action t] runs the default handler passed to {!make}. *)
|
(** [run_default_action t] runs the default handler passed to {!make}. *)
|
||||||
end
|
end
|
||||||
|
|
||||||
(** {2 Pre-defined mocks *)
|
(** {2 Pre-defined mocks} *)
|
||||||
|
|
||||||
(** Mock {!Eio.Flow} sources and sinks. *)
|
(** Mock {!Eio.Flow} sources and sinks. *)
|
||||||
module Flow : sig
|
module Flow : sig
|
||||||
@ -148,3 +148,10 @@ module Net : sig
|
|||||||
unit
|
unit
|
||||||
(** [on_accept socket actions] configures how to respond when the server calls "accept". *)
|
(** [on_accept socket actions] configures how to respond when the server calls "accept". *)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
(** {2 Backend for mocks}
|
||||||
|
|
||||||
|
The mocks can be used with any backend, but if you don't need any IO then you can use this one
|
||||||
|
to avoid a dependency on eio_main. *)
|
||||||
|
|
||||||
|
module Backend = Backend
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
```ocaml
|
```ocaml
|
||||||
# #require "eio";;
|
# #require "eio";;
|
||||||
# #require "eio.mock";;
|
|
||||||
```
|
```
|
||||||
```ocaml
|
```ocaml
|
||||||
module R = Eio.Buf_read;;
|
module R = Eio.Buf_read;;
|
||||||
@ -599,6 +598,8 @@ Exception: Failure "Unexpected data after parsing (at offset 4)".
|
|||||||
## Test using mock flow
|
## Test using mock flow
|
||||||
|
|
||||||
```ocaml
|
```ocaml
|
||||||
|
# #require "eio.mock";;
|
||||||
|
|
||||||
# let flow = Eio_mock.Flow.make "flow" in
|
# let flow = Eio_mock.Flow.make "flow" in
|
||||||
Eio_mock.Flow.on_read flow [
|
Eio_mock.Flow.on_read flow [
|
||||||
`Return "foo\nba";
|
`Return "foo\nba";
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
```ocaml
|
```ocaml
|
||||||
# #require "eio_main";;
|
|
||||||
# #require "eio.mock";;
|
# #require "eio.mock";;
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -14,7 +13,7 @@ let stdout = Eio_mock.Flow.make "stdout"
|
|||||||
## Flows
|
## Flows
|
||||||
|
|
||||||
```ocaml
|
```ocaml
|
||||||
# Eio_main.run @@ fun _ ->
|
# Eio_mock.Backend.run @@ fun _ ->
|
||||||
Eio_mock.Flow.on_read stdin [
|
Eio_mock.Flow.on_read stdin [
|
||||||
`Return "chunk1";
|
`Return "chunk1";
|
||||||
`Return "chunk2";
|
`Return "chunk2";
|
||||||
@ -47,7 +46,7 @@ let echo_server ~net addr =
|
|||||||
The server handles a connection:
|
The server handles a connection:
|
||||||
|
|
||||||
```ocaml
|
```ocaml
|
||||||
# Eio_main.run @@ fun _ ->
|
# Eio_mock.Backend.run @@ fun _ ->
|
||||||
let net = Eio_mock.Net.make "mocknet" in
|
let net = Eio_mock.Net.make "mocknet" in
|
||||||
let listening_socket = Eio_mock.Net.listening_socket "tcp/80" in
|
let listening_socket = Eio_mock.Net.listening_socket "tcp/80" in
|
||||||
Eio_mock.Net.on_listen net [`Return listening_socket];
|
Eio_mock.Net.on_listen net [`Return listening_socket];
|
||||||
@ -66,3 +65,37 @@ The server handles a connection:
|
|||||||
+tcp/80: closed
|
+tcp/80: closed
|
||||||
- : unit = ()
|
- : unit = ()
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
`Eio_mock.Backend` supports forking, tracing, suspending and cancellation:
|
||||||
|
|
||||||
|
```ocaml
|
||||||
|
# Eio_mock.Backend.run @@ fun () ->
|
||||||
|
let s = Eio.Stream.create 1 in
|
||||||
|
try
|
||||||
|
Fiber.both
|
||||||
|
(fun () ->
|
||||||
|
for x = 1 to 3 do
|
||||||
|
traceln "Sending %d" x;
|
||||||
|
Eio.Stream.add s x
|
||||||
|
done;
|
||||||
|
raise Exit
|
||||||
|
)
|
||||||
|
(fun () ->
|
||||||
|
while true do
|
||||||
|
traceln "Got %d" (Eio.Stream.take s)
|
||||||
|
done
|
||||||
|
)
|
||||||
|
with Exit ->
|
||||||
|
traceln "Finished!";;
|
||||||
|
+Sending 1
|
||||||
|
+Sending 2
|
||||||
|
+Got 1
|
||||||
|
+Got 2
|
||||||
|
+Sending 3
|
||||||
|
+Got 3
|
||||||
|
+Finished!
|
||||||
|
- : unit = ()
|
||||||
|
```
|
||||||
|
|
||||||
|
|||||||
@ -1,14 +1,14 @@
|
|||||||
# Setting up the environment
|
# Setting up the environment
|
||||||
|
|
||||||
```ocaml
|
```ocaml
|
||||||
# #require "eio_main";;
|
# #require "eio.mock";;
|
||||||
```
|
```
|
||||||
|
|
||||||
```ocaml
|
```ocaml
|
||||||
open Eio.Std
|
open Eio.Std
|
||||||
|
|
||||||
let run (fn : Switch.t -> unit) =
|
let run (fn : Switch.t -> _) =
|
||||||
Eio_main.run @@ fun _e ->
|
Eio_mock.Backend.run @@ fun () ->
|
||||||
Switch.run fn
|
Switch.run fn
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -125,10 +125,7 @@ Exception: Failure "Cancel".
|
|||||||
You can't use a switch after leaving its scope:
|
You can't use a switch after leaving its scope:
|
||||||
|
|
||||||
```ocaml
|
```ocaml
|
||||||
# let sw =
|
# let sw = run Fun.id;;
|
||||||
let x = ref None in
|
|
||||||
run (fun sw -> x := Some sw);
|
|
||||||
Option.get !x;;
|
|
||||||
val sw : Switch.t = <abstr>
|
val sw : Switch.t = <abstr>
|
||||||
# Switch.check sw;;
|
# Switch.check sw;;
|
||||||
Exception: Invalid_argument "Switch finished!".
|
Exception: Invalid_argument "Switch finished!".
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user