-
Notifications
You must be signed in to change notification settings - Fork 38
Add tcp servers and clients to standard library #1058
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
phischu
wants to merge
24
commits into
master
Choose a base branch
from
stdlib/network
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
24 commits
Select commit
Hold shift + click to select a range
8a25978
TCP client on llvm
phischu 526960e
Start working TCP server
phischu d93eecb
Fix formatting
phischu eec4c3c
Accept callback in accept
phischu a91a6f7
Remove traces of evidence
phischu 05e4e27
Share handler
phischu 85b8d4c
Run boxes only
phischu cef36c7
Fix bug in spawn and document todo
phischu 8fcfb7c
Add test
phischu 57721cd
Fix stdlib/network (#1063)
marvinborner 325798f
Only free stuff in callback
phischu 6655bc1
Close listener in case of error
phischu 1e23d5c
Delete duplicate callback
phischu 0278cc9
Don't listen in listen
phischu 55deb86
Resume continuation instead of erasing it
phischu 9086769
Checkfile
phischu 09886bb
Rename and fix tests
phischu 67df861
Shutdown and close never fail
phischu 591fa3e
Resume last in shutdown
phischu 9bf7c6f
Resume last in error cases
phischu d81caf5
Make bind non-async
phischu 727f782
Add documentation
phischu ca34e62
New syntax for async
phischu d95ab0d
Streaming client
phischu File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
55 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import examples/benchmarks/runner | ||
|
||
import io | ||
import io/error | ||
import io/network | ||
import bytearray | ||
|
||
def run(n: Int) = { | ||
with on[IOError].panic | ||
|
||
val listener = bind("127.0.0.1", 8080); | ||
spawn(box { | ||
with on[IOError].panic | ||
listen(listener, box { connection => | ||
with on[IOError].panic | ||
val message = "hello world" | ||
var buffer = bytearray::fromString(message) | ||
write(connection, buffer, 0, buffer.size()) | ||
close(connection) | ||
})}); | ||
|
||
val results = array::build(n) { i => | ||
promise(box { | ||
with on[IOError].result | ||
val connection = connect("127.0.0.1", 8080) | ||
var buffer = bytearray::allocate(4096) | ||
val number = read(connection, buffer, 0, 4096) | ||
close(connection) | ||
number | ||
}) | ||
}; | ||
|
||
var total = 0; | ||
results.foreach { number => | ||
total = total + number.await.value | ||
}; | ||
|
||
shutdown(listener) | ||
|
||
return total | ||
} | ||
|
||
def main() = benchmark(5){run} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,84 +1,155 @@ | ||
module io/network | ||
|
||
import stream | ||
import bytearray | ||
import io | ||
|
||
namespace js { | ||
extern jsNode """ | ||
const net = require('node:net'); | ||
import io/error | ||
|
||
function listen(server, port, host, listener) { | ||
server.listen(port, host); | ||
server.on('connection', listener); | ||
} | ||
""" | ||
|
||
extern type JSServer // = net.Server | ||
extern type JSSocket // = net.Socket | ||
extern def server() at io: JSServer = | ||
jsNode "net.createServer()" | ||
extern def listen(server: JSServer, port: Int, host: String, listener: JSSocket => Unit at {io, async, global}) at io: Unit = | ||
jsNode "listen(${server}, ${port}, ${host}, (socket) => $effekt.runToplevel((ks, k) => (${listener})(socket, ks, k)))" | ||
def client[R](host: String, port: Int) { stream: () => R / {read[Byte], emit[Byte]} }: R / Exception[IOError] = { | ||
|
||
extern def send(socket: JSSocket, data: ByteArray) at async: Unit = | ||
jsNode "$effekt.capture(callback => ${socket}.write(${data}, callback))" | ||
val connection = connect(host, port); | ||
with on[IOError].finalize { close(connection) } | ||
|
||
extern def receive(socket: JSSocket) at async: ByteArray = | ||
jsNode "$effekt.capture(callback => ${socket}.once('data', callback))" | ||
val writeBuffer = bytearray::allocate(4096) | ||
var writeBufferOffset = 0 | ||
|
||
extern def end(socket: JSSocket) at async: Unit = | ||
jsNode "$effekt.capture(k => ${socket}.end(k))" | ||
} | ||
val readBuffer = bytearray::allocate(4096) | ||
var readBufferOffset = 0 | ||
var readBufferFilled = 0 | ||
|
||
interface Socket { | ||
def send(message: ByteArray): Unit | ||
def receive(): ByteArray | ||
def end(): Unit | ||
} | ||
def push(i: Int, n: Int): Unit = { | ||
val r = write(connection, writeBuffer, i, n) | ||
if (r < n) { | ||
push(i + r, n - r) | ||
} | ||
} | ||
|
||
def pull(): Int = { | ||
read(connection, readBuffer, 0, readBuffer.size) match { | ||
case 0 => pull() | ||
case n => n | ||
} | ||
} | ||
|
||
def server(host: String, port: Int, handler: () => Unit / Socket at {io, async, global}): Unit = { | ||
val server = js::server(); | ||
js::listen(server, port, host, box { socket => | ||
println("New connection") | ||
spawn(box { | ||
try handler() | ||
with Socket { | ||
def send(message) = | ||
resume(js::send(socket, message)) | ||
def receive() = | ||
resume(js::receive(socket)) | ||
def end() = | ||
resume(js::end(socket)) | ||
} | ||
}) | ||
}) | ||
try { | ||
val r = stream() | ||
push(0, writeBufferOffset) | ||
return r | ||
} with emit[Byte] { (byte) => | ||
if (writeBufferOffset >= writeBuffer.size) { | ||
push(0, writeBuffer.size) | ||
writeBufferOffset = 0 | ||
} | ||
writeBuffer.unsafeSet(writeBufferOffset, byte) | ||
writeBufferOffset = writeBufferOffset + 1 | ||
resume(()) | ||
} with read[Byte] { () => | ||
if (readBufferOffset >= readBufferFilled) { | ||
val n = pull() | ||
readBufferOffset = 0 | ||
readBufferFilled = n | ||
} | ||
val byte = unsafeGet(readBuffer, readBufferOffset) | ||
readBufferOffset = readBufferOffset + 1 | ||
resume { return byte } | ||
} | ||
} | ||
|
||
|
||
namespace examples { | ||
def helloWorldApp(): Unit / Socket = { | ||
val request = do receive().toString; | ||
|
||
println("Received a request: " ++ request) | ||
/// A TCP handle. Should not be inspected. | ||
type Connection = Int | ||
|
||
def respond(s: String): Unit / Socket = | ||
do send(s.fromString) | ||
/// Reads data from a TCP connection into a buffer at the given offset. | ||
def read(handle: Connection, buffer: ByteArray, offset: Int, size: Int): Int / Exception[IOError] = | ||
internal::checkResult(internal::read(handle, buffer, offset, size)) | ||
|
||
if (request.startsWith("GET /")) { | ||
respond("HTTP/1.1 200 OK\r\n\r\nHello from Effekt!") | ||
} else { | ||
respond("HTTP/1.1 400 Bad Request\r\n\r\n") | ||
} | ||
do end() | ||
} | ||
/// Writes data from a buffer at the given offset to a TCP connection. | ||
def write(handle: Connection, buffer: ByteArray, offset: Int, size: Int): Int / Exception[IOError] = | ||
internal::checkResult(internal::write(handle, buffer, offset, size)) | ||
|
||
// A server that just shows "Hello from Effekt!" on localhost:8080 | ||
def main() = { | ||
val port = 8080 | ||
println("Starting server on http://localhost:" ++ port.show) | ||
/// Establishes a TCP connection to the specified host and port. | ||
def connect(host: String, port: Int): Connection / Exception[IOError] = | ||
internal::checkResult(internal::connect(host, port)) | ||
|
||
server("localhost", port, box { | ||
helloWorldApp() | ||
}) | ||
} | ||
/// Closes a TCP connection and releases associated resources. | ||
def close(handle: Connection): Unit = | ||
internal::close(handle) | ||
|
||
/// A TCP listener. Should not be inspected. | ||
type Listener = Int | ||
|
||
/// Creates a TCP listener bound to the specified host and port. | ||
def bind(host: String, port: Int): Listener / Exception[IOError] = | ||
internal::checkResult(internal::bind(host, port)) | ||
|
||
/// Starts listening for incoming connections and handles them with the provided handler function. | ||
/// Runs until `shutdown` is called on this `Listener`. | ||
def listen(listener: Listener, handler: Connection => Unit at {io, async, global}): Unit / Exception[IOError] = { | ||
internal::checkResult(internal::listen(listener, handler)); () | ||
} | ||
|
||
/// Stops a TCP listener and releases associated resources. | ||
def shutdown(listener: Listener): Unit = | ||
internal::shutdown(listener) | ||
|
||
namespace internal { | ||
|
||
extern llvm """ | ||
declare void @c_tcp_connect(%Pos, %Int, %Stack) | ||
declare void @c_tcp_read(%Int, %Pos, %Int, %Int, %Stack) | ||
declare void @c_tcp_write(%Int, %Pos, %Int, %Int, %Stack) | ||
declare void @c_tcp_close(%Int, %Stack) | ||
declare void @c_tcp_bind(%Pos, %Int, %Int, %Stack) | ||
declare void @c_tcp_listen(%Int, %Pos, %Stack) | ||
declare void @c_tcp_shutdown(%Int, %Stack) | ||
""" | ||
|
||
extern def connect(host: String, port: Int) at async: Int = | ||
llvm """ | ||
call void @c_tcp_connect(%Pos ${host}, %Int ${port}, %Stack %stack) | ||
ret void | ||
""" | ||
|
||
/// The buffer must be kept alive by using it after the call | ||
extern def read(handle: Int, buffer: ByteArray, offset: Int, size: Int) at async: Int = | ||
llvm """ | ||
call void @c_tcp_read(%Int ${handle}, %Pos ${buffer}, %Int ${offset}, %Int ${size}, %Stack %stack) | ||
ret void | ||
""" | ||
|
||
/// The buffer must be kept alive by using it after the call | ||
extern def write(handle: Int, buffer: ByteArray, offset: Int, size: Int) at async: Int = | ||
llvm """ | ||
call void @c_tcp_write(%Int ${handle}, %Pos ${buffer}, %Int ${offset}, %Int ${size}, %Stack %stack) | ||
ret void | ||
""" | ||
|
||
extern def close(handle: Int) at async: Unit = | ||
llvm """ | ||
call void @c_tcp_close(%Int ${handle}, %Stack %stack) | ||
ret void | ||
""" | ||
|
||
extern def bind(host: String, port: Int) at io: Int = | ||
llvm """ | ||
%result = call %Int @c_tcp_bind(%Pos ${host}, %Int ${port}) | ||
ret %Int %result | ||
""" | ||
|
||
extern def listen(listener: Int, handler: Int => Unit at {io, async, global}) at async: Int = | ||
llvm """ | ||
call void @c_tcp_listen(%Int ${listener}, %Pos ${handler}, %Stack %stack) | ||
ret void | ||
""" | ||
|
||
extern def shutdown(handle: Int) at async: Unit = | ||
llvm """ | ||
call void @c_tcp_shutdown(%Int ${handle}, %Stack %stack) | ||
ret void | ||
""" | ||
|
||
} | ||
|
||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we make it an extern type so that it cannot be inspected?
(also applies to
Listener
below)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I don't like it (because then it is a
Pos
) but I guess I'll have to do it, because the representation injs
is different.Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Approaching the problem from the other side: does it have to be a
Pos
? We could let the user choose, no?Couldn't we fix this "properly" by some
= llvm "i64"
annotation there instead? 🎣