Skip to content

std.socket API proposal #405

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
wants to merge 1 commit into
base: master
Choose a base branch
from
Open

std.socket API proposal #405

wants to merge 1 commit into from

Conversation

yne
Copy link

@yne yne commented May 8, 2025

The goal of this PR is to start a discussion about the socket API.

My goal is to use QuickJS as cosmo/portable CGI server without relying on external lib or forks.

I took inspiration from other std syscall wrapper to do this proposal

  • fd = std.socket(domain=AF_INET, type=SOCK_STREAM, protocol=0)
  • err = std.bind(sock, {...sockaddr})
  • err = std.listen(sock)
  • fd = std.accept(sock)
  • err=connect(sock {...sockaddr})

I've tested with the following http server example

//TODO: close socket_srv from os.signal()
///@ts-check
import * as os from "os";
import * as std from "std";

function must(ret=0) { if (ret < 0) { console.log(std.strerror(-ret)); std.exit(-ret) } };

const sock_srv = std.socket();
const port = 8080;
must(std.bind(sock_srv, { port }))
must(std.listen(sock_srv))
console.log(`Listening on http://0.0.0.0:${port} ...`)

while (true) {
    const [sock_cli] = std.accept(sock_srv); // don't care about sockaddr
    const sock_cli_r = std.fdopen(sock_cli, "r")
    const sock_cli_w = std.fdopen(sock_cli, "w") // wrap with os.dup() ?

    const [method, path, http_ver] = sock_cli_r.getline()?.trimEnd("\r").split(' ')
    const headers = new Map()
    for (; ;) {
        const line = sock_cli_r.getline()?.trimEnd("\r")
        if (!line) break
        const index = line.indexOf(': ')
        headers.set(line.slice(0, index), line.slice(index + 2))
    }

    sock_cli_w.puts([
        'HTTP/1.1 200 OK',
        'Content-Type: application/json',
        '',
        JSON.stringify({
            method,
            path,
            http_ver,
            headers: Object.fromEntries(headers.entries()),
            scriptArgs,
            platform: os.platform,
            now: new Date(),
        })
    ].join('\r\n'))
    sock_cli_w.close();
    if (path == "/quit") break;
}

os.close(sock_srv) // ret=0 but won't prevent ALREADY IN USE error => added SO_REUSEADDR in qjs
// USAGE: curl -s 0:8080 | jq
{
  "method": "GET",
  "path": "/",
  "http_ver": "HTTP/1.1",
  "headers": {
    "Host": "0.0.0.0:8080",
    "User-Agent": "curl/8.5.0",
    "Accept": "*/*"
  },
  "scriptArgs": [
    "examples/socket.js"
  ],
  "platform": "linux",
  "now": "2025-05-08T18:31:07.402Z"
}

I'm confident about the return value API, but I'm open to feedback about:

  • allowing implicit args (socket() currently return a TCP fd, listen() default to a backlog of 10)
  • shall we use a specific SockAddr() constructor (with .parse() and .toString()) instead of the current plain {address:number,port:number} object used in bind() and connect()
  • are socket more os related than std
  • remove the SO_REUSEADDR I added, and add a setsockopt wrapper (but just for this usecase ?)

Once agreed, I'll add testing and add all required JS_IsException ...

implement the following
- `fd = std.socket(domain=AF_INET, type=SOCK_STREAM, protocol=0)`
- `err = std.bind(sock, {...sockaddr})`
- `err = std.listen(sock)`
- `fd = std.accept(sock)`
- `err=connect(sock {...sockaddr})`

provide a http server as example
@yne yne marked this pull request as draft May 8, 2025 18:26
@yne yne changed the title [draft] std.socket API std.socket API proposa' May 9, 2025
@yne yne changed the title std.socket API proposa' std.socket API proposal May 9, 2025
@yne yne marked this pull request as ready for review May 11, 2025 04:25
@bellard
Copy link
Owner

bellard commented May 11, 2025

I am interested by the feature. Here are some suggestions:

  • the functions should be in "os" instead of "std"
  • no implicit args for socket()
  • implicit arg for listen is acceptable
  • use a plain object for sockaddr. The "family" field is optional (AF_INET by default). "address" should be renamed to "addr" and should be a string containing the IPv4 or IPv6 address. AF_INET6 should be supported too
  • remove SO_REUSEADDR and add setsockopt() and getsockopt()
  • For better portability it is useful to add recv, recvfrom, send, sendto and shutdown
  • In your example, it is better to avoid using fdopen() on sockets ('FILE *' are designed to work on files)
  • a wrapper to getaddrinfo() would be needed (take a hostname as input, returns a sockaddr object)
  • for win32 compilation without cosmolibc some specific support might be needed if you want os.write/os.read/os.close to work with socket handles

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants