Support Racket on Chez Scheme

Remove the dependency on Racket BC's C API.
We now build a plain C shared library and
interact with it through the FFI.

Closes https://github.com/tonyg/racket-unix-signals/issues/1
This commit is contained in:
Philip McGrath 2020-02-23 14:12:14 -05:00
parent 7231d17617
commit a0c50918da
No known key found for this signature in database
GPG Key ID: CA03638DFA3F1C7A
8 changed files with 286 additions and 133 deletions

50
.travis.yml Normal file
View File

@ -0,0 +1,50 @@
language: c
# Based on: https://github.com/greghendershott/travis-racket
dist: bionic
env:
global:
# Supply a global RACKET_DIR environment variable. This is where
# Racket will be installed. A good idea is to use ~/racket because
# that doesn't require sudo to install and is therefore compatible
# with Travis CI's newer container infrastructure.
- RACKET_DIR=~/racket
matrix:
# Supply at least one RACKET_VERSION environment variable. This is
# used by the install-racket.sh script (run at before_install,
# below) to select the version of Racket to download and install.
#
# Supply more than one RACKET_VERSION (as in the example below) to
# create a Travis-CI build matrix to test against multiple Racket
# versions.
- RACKET_VERSION=7.6
- RACKET_VERSION=HEAD
- RACKET_VERSION=HEADCS
matrix:
allow_failures:
# - env: RACKET_VERSION=HEAD
fast_finish: true
before_install:
- git clone https://github.com/greghendershott/travis-racket.git ~/travis-racket
- cat ~/travis-racket/install-racket.sh | bash # pipe to bash not sh!
- export PATH="${RACKET_DIR}/bin:${PATH}" #install-racket.sh can't set for us
install:
- raco pkg install -j 4 --auto --name unix-signals
before_script:
# Here supply steps such as raco make, raco test, etc. You can run
# `raco pkg install --deps search-auto` to install any required
# packages without it getting stuck on a confirmation prompt.
script:
- raco test -x -p unix-signals
after_success:
- raco setup --check-pkg-deps --pkgs unix-signals
- raco pkg install --deps search-auto cover cover-coveralls
- raco cover -b -f coveralls -d $TRAVIS_BUILD_DIR/coverage .

View File

@ -1,5 +1,11 @@
#lang setup/infotab #lang info
(define pkg-name 'unix-signals)
(define collection 'multi) (define collection 'multi)
(define deps '("base" "rackunit-lib" "make")) (define deps
(define build-deps '("racket-doc" "scribble-lib")) '(["base" #:version "6.12"]
(define homepage "https://github.com/tonyg/racket-unix-signals") "dynext-lib"))
(define build-deps
'("racket-doc"
"scribble-lib"))
(define homepage
"https://github.com/tonyg/racket-unix-signals")

View File

@ -1,4 +1,4 @@
#lang setup/infotab #lang info
(define scribblings '(("unix-signals.scrbl" ()))) (define scribblings '(("unix-signals.scrbl" ())))
(define pre-install-collection "private/install.rkt") (define pre-install-collection "private/install.rkt")
(define compile-omit-files '("private/install.rkt")) (define compile-omit-files '("private/install.rkt"))

View File

@ -1,48 +1,137 @@
#lang racket/base #lang racket/base
(provide next-signal-evt (require ffi/unsafe
read-signal ffi/unsafe/port
lookup-signal-number ffi/unsafe/define
racket/port
(rename-in racket/contract
[-> ->/c])
(only-in racket/os getpid))
(provide lookup-signal-number
lookup-signal-name lookup-signal-name
capture-signal!
ignore-signal!
release-signal!
getpid getpid
send-signal!) (contract-out
;; These may not do what you want,
;; but they shouldn't break invariants
;; of the runtime system:
[next-signal-evt
(evt/c byte?)]
[read-signal
(->/c byte?)])
;; These are unsafe:
(protect-out
(contract-out
[capture-signal!
(->/c (or/c symbol? fixnum?) boolean?)]
[ignore-signal!
(->/c (or/c symbol? fixnum?) boolean?)]
[release-signal!
(->/c (or/c symbol? fixnum?) boolean?)]
[send-signal!
(->/c fixnum? (or/c symbol? fixnum?) boolean?)])))
(require (only-in racket/os getpid)) (define (local-lib-dirs)
(require "private/unix-signals-extension.rkt") ;; FIXME: There's probably a better way to do this with
;; define-runtime-path and cross-system-library-subpath,
;; but this is what the bcrypt package is doing.
(list (build-path (collection-path "unix-signals")
"private"
"compiled"
"native"
(system-library-subpath #f))))
(define signal-fd (get-signal-fd)) (define libracket-unix-signals
(ffi-lib "libracket_unix_signals" #:get-lib-dirs local-lib-dirs))
(define next-signal-evt (define-ffi-definer define-unix libracket-unix-signals
(handle-evt signal-fd (lambda (_) (read-signal)))) #:default-make-fail make-not-available)
(define (read-signal) (read-byte signal-fd)) ;; TODO: should we be using #:lock-name, #:in-original-place?,
;; or other options for some of these _fun types?
(define signals-by-name (get-signal-names)) (define-values [signals-by-name signals-by-number]
(define signals-by-number (let ([signals-by-name #hasheq()]
(for/hash [((name number) (in-hash signals-by-name))] (values number name))) [signals-by-number #hasheq()])
;; "two fixnums that are `=` are also the same according to `eq?`"
(define-unix racket_unix_signals_init
(_fun -> _stdbool))
(unless (racket_unix_signals_init)
(error 'unix-signals "error initializing foreign library"))
(define-unix prim_signal_names_for_each
(_fun (_fun _symbol _fixint -> _void)
-> _void))
(prim_signal_names_for_each
(λ (name num)
(set! signals-by-name (hash-set signals-by-name name num))
(set! signals-by-number (hash-set signals-by-number num name))))
(values signals-by-name signals-by-number)))
(define (lookup-signal-number sym) (hash-ref signals-by-name sym #f)) (define (lookup-signal-number sym)
(define (lookup-signal-name num) (hash-ref signals-by-number num #f)) (hash-ref signals-by-name sym #f))
(define (lookup-signal-name num)
(hash-ref signals-by-number num #f))
(define (name->signum who n)
(cond (define-values [read-signal next-signal-evt]
[(symbol? n) (or (lookup-signal-number n) (let ()
(error who "Unknown signal name ~a" n))] (define-unix prim_get_signal_fd
[(fixnum? n) n] (_fun -> _int))
[else (error who "Expects signal name symbol or signal number; got ~v" n)])) (define signal-fd-in
;; NB: closing this port closes the file descriptor
;; (that was already true with scheme_make_fd_input_port)
(unsafe-file-descriptor->port (prim_get_signal_fd)
'signal-fd
'(read)))
(define (assert-not-eof who v)
(if (eof-object? v)
(raise (exn:fail:read:eof
(format "~a: internal error;\n unexpected eof" who)
(current-continuation-marks)
null))
v))
(define (read-signal)
(assert-not-eof 'read-signal (read-byte signal-fd-in)))
(values read-signal
(wrap-evt (read-bytes-evt 1 signal-fd-in)
(λ (bs)
(assert-not-eof 'next-signal-evt bs)
(bytes-ref bs 0))))))
(define name->signum
(case-lambda
[(who sig)
(name->signum who #f sig)]
[(who ?pid sig)
(cond
[(fixnum? sig)
sig]
[(lookup-signal-number sig)]
[else
(error who
"unknown signal name\n given: ~e~a\n known names...:~a"
sig
(if ?pid (format "\n pid: ~e" ?pid) "")
(apply string-append
(hash-map signals-by-name
(λ (name _num)
(format "\n ~e" name))
'ordered)))])]))
(define-unix prim_capture_signal
(_fun _fixint _fixint -> _stdbool))
(define (capture-signal! sig) (define (capture-signal! sig)
(set-signal-handler! (name->signum 'capture-signal! sig) 0)) (prim_capture_signal (name->signum 'capture-signal! sig) 0))
(define (ignore-signal! sig) (define (ignore-signal! sig)
(set-signal-handler! (name->signum 'capture-signal! sig) 1)) (prim_capture_signal (name->signum 'ignore-signal! sig) 1))
(define (release-signal! sig) (define (release-signal! sig)
(set-signal-handler! (name->signum 'capture-signal! sig) 2)) (prim_capture_signal (name->signum 'release-signal! sig) 2))
(define-unix prim_send_signal
(_fun _fixint _fixint -> _stdbool))
(define (send-signal! pid sig) (define (send-signal! pid sig)
(when (not (fixnum? pid)) (error 'send-signal! "Expected fixnum pid; got ~v" pid)) (prim_send_signal pid (name->signum 'send-signal! pid sig)))
(lowlevel-send-signal! pid (name->signum 'send-signal! sig)))

View File

@ -1,19 +1,32 @@
#lang racket/base #lang racket/base
(require make/setup-extension) (require dynext/file
dynext/link
racket/file)
(provide pre-installer) (provide pre-installer)
(define (pre-installer collections-top-path our-path) ;; Used by "../info.rkt" (so this-collection-path is "..").
(pre-install our-path
(build-path our-path "private") ;; Heavily based on Sam Tobin-Hochstadt's bcrypt/private/install.rkt
"unix-signals-extension.c" ;; https://github.com/samth/bcrypt.rkt
"."
'() (define (pre-installer collections-top-path this-collection-path)
'() (define unix-signals/private/
'() (build-path this-collection-path "private"))
'() (parameterize ([current-directory unix-signals/private/]
'() [current-use-mzdyn #f])
'() (define racket_unix_signals.c
(lambda (thunk) (thunk)) (build-path unix-signals/private/ "racket_unix_signals.c"))
#t)) (define libracket_unix_signals.so
(build-path unix-signals/private/
"compiled"
"native"
(system-library-subpath #f)
(append-extension-suffix "libracket_unix_signals")))
(when (file-exists? libracket_unix_signals.so)
(delete-file libracket_unix_signals.so))
(make-parent-directory* libracket_unix_signals.so)
(link-extension #f ;; not quiet
(list racket_unix_signals.c)
libracket_unix_signals.so)))

View File

@ -2,6 +2,7 @@
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <stdint.h> #include <stdint.h>
#include <stdbool.h>
#include <fcntl.h> #include <fcntl.h>
#include <unistd.h> #include <unistd.h>
@ -9,11 +10,12 @@
#include <sys/types.h> #include <sys/types.h>
#include "escheme.h"
/* This implementation uses djb's "self-pipe trick". /* This implementation uses djb's "self-pipe trick".
* See http://cr.yp.to/docs/selfpipe.html. */ * See http://cr.yp.to/docs/selfpipe.html. */
/* TODO: Communicate errno to Racket rather than using `perror`.
* See Racket's `saved-errno` and `lookup-errno`. */
static int self_pipe_initialized = 0; static int self_pipe_initialized = 0;
static int self_pipe_read_end = -1; static int self_pipe_read_end = -1;
static int self_pipe_write_end = -1; static int self_pipe_write_end = -1;
@ -71,20 +73,13 @@ static void signal_handler_fn(int signum) {
} }
} }
Scheme_Object *prim_get_signal_fd(int argc, Scheme_Object **argv) { int prim_get_signal_fd(void) {
if (self_pipe_read_end == -1) { return self_pipe_read_end;
return scheme_false;
} else {
return scheme_make_fd_input_port(self_pipe_read_end, scheme_intern_symbol("signal-fd"), 0, 0);
}
} }
Scheme_Object *prim_get_signal_names(int argc, Scheme_Object **argv) { void prim_signal_names_for_each(void (*callback)(char*, int)) {
Scheme_Hash_Table *ht;
ht = scheme_make_hash_table(SCHEME_hash_ptr); #define ADD_SIGNAL_NAME(n) callback(#n, n)
#define ADD_SIGNAL_NAME(n) scheme_hash_set(ht, scheme_intern_symbol(#n), scheme_make_integer(n))
/* POSIX.1-1990 */ /* POSIX.1-1990 */
ADD_SIGNAL_NAME(SIGHUP); ADD_SIGNAL_NAME(SIGHUP);
@ -127,80 +122,51 @@ Scheme_Object *prim_get_signal_names(int argc, Scheme_Object **argv) {
#undef ADD_SIGNAL_NAME #undef ADD_SIGNAL_NAME
return (Scheme_Object *) ht;
} }
Scheme_Object *prim_capture_signal(int argc, Scheme_Object **argv) { bool prim_capture_signal(int signum, int code) {
int signum = SCHEME_INT_VAL(argv[0]);
int code = SCHEME_INT_VAL(argv[1]);
switch (code) { switch (code) {
case 0: case 0:
if (XFORM_HIDE_EXPR(signal(signum, signal_handler_fn) == SIG_ERR)) { if (signal(signum, signal_handler_fn) == SIG_ERR) {
perror("unix-signals-extension signal(2) install"); perror("unix-signals-extension signal(2) install");
return scheme_false; return false;
} }
break; break;
case 1: case 1:
if (XFORM_HIDE_EXPR(signal(signum, SIG_IGN) == SIG_ERR)) { if (signal(signum, SIG_IGN) == SIG_ERR) {
perror("unix-signals-extension signal(2) ignore"); perror("unix-signals-extension signal(2) ignore");
return scheme_false; return false;
} }
break; break;
case 2: case 2:
if (XFORM_HIDE_EXPR(signal(signum, SIG_DFL) == SIG_ERR)) { if (signal(signum, SIG_DFL) == SIG_ERR) {
perror("unix-signals-extension signal(2) default"); perror("unix-signals-extension signal(2) default");
return scheme_false; return false;
} }
break; break;
default: default:
return scheme_false; return false;
} }
return scheme_true; return true;
} }
Scheme_Object *prim_send_signal(int argc, Scheme_Object **argv) { bool prim_send_signal(pid_t pid, int sig) {
pid_t pid = SCHEME_INT_VAL(argv[0]);
int sig = SCHEME_INT_VAL(argv[1]);
if (kill(pid, sig) == -1) { if (kill(pid, sig) == -1) {
perror("unix-signals-extension kill(2)"); perror("unix-signals-extension kill(2)");
return scheme_false; return false;
} }
return scheme_true; return true;
} }
Scheme_Object *scheme_reload(Scheme_Env *env) { bool racket_unix_signals_init(void) {
Scheme_Env *module_env;
Scheme_Object *proc;
if (!self_pipe_initialized) { if (!self_pipe_initialized) {
if (setup_self_pipe() == -1) { if (setup_self_pipe() == -1) {
return scheme_false; return false;
} }
self_pipe_initialized = 1; self_pipe_initialized = 1;
} }
module_env = scheme_primitive_module(scheme_intern_symbol("unix-signals-extension"), env); return true;
proc = scheme_make_prim_w_arity(prim_get_signal_fd, "get-signal-fd", 0, 0);
scheme_add_global("get-signal-fd", proc, module_env);
proc = scheme_make_prim_w_arity(prim_get_signal_names, "get-signal-names", 0, 0);
scheme_add_global("get-signal-names", proc, module_env);
proc = scheme_make_prim_w_arity(prim_capture_signal, "set-signal-handler!", 2, 2);
scheme_add_global("set-signal-handler!", proc, module_env);
proc = scheme_make_prim_w_arity(prim_send_signal, "lowlevel-send-signal!", 2, 2);
scheme_add_global("lowlevel-send-signal!", proc, module_env);
scheme_finish_primitive_module(module_env);
return scheme_void;
}
Scheme_Object *scheme_initialize(Scheme_Env *env) {
return scheme_reload(env);
}
Scheme_Object *scheme_module_name() {
return scheme_intern_symbol("unix-signals-extension");
} }

18
unix-signals/test.rkt Executable file
View File

@ -0,0 +1,18 @@
#!/usr/bin/env racket
#lang racket/base
(module test '#%kernel)
(require unix-signals)
(module+ main
(capture-signal! 'SIGUSR1)
(capture-signal! 'SIGUSR2)
(printf "Try 'kill -USR1 ~a' and 'kill -USR2 ~a'\n" (getpid) (getpid))
(let loop ([mode #t])
(define signum
(if mode
(read-signal)
(sync next-signal-evt)))
(printf "Received signal ~v (name ~v)\n" signum (lookup-signal-name signum))
(loop (not mode))))

View File

@ -6,30 +6,25 @@
@title{unix-signals} @title{unix-signals}
@author[(author+email "Tony Garnock-Jones" "tonyg@leastfixedpoint.com")] @author[(author+email "Tony Garnock-Jones" "tonyg@leastfixedpoint.com")]
If you find that this library lacks some feature you need, or you have @(defmodule unix-signals)
a suggestion for improving it, please don't hesitate to
@link["mailto:tonyg@leastfixedpoint.com"]{get in touch with me}!
@section{Introduction} @nested[#:style 'inset]{
If you find that this library lacks some feature you need, or you have
a suggestion for improving it, please don't hesitate to
@link["mailto:tonyg@leastfixedpoint.com"]{get in touch with me}!
}
This library provides a means of sending and receiving Unix signals to This library provides a means of sending and receiving Unix signals to
Racket programs. Racket programs.
Be warned that attempting to receive certain signals used by the @(define unsafe
Racket runtime is dangerous, as the code here will conflict with the @tech[#:doc '(lib "scribblings/reference/reference.scrbl")]{unsafe})
@bold{Be warned} that attempting to receive certain signals used by the
Racket runtime is @|unsafe|, as the code here will conflict with the
code in Racket itself. code in Racket itself.
@section{What to require} @section{Waiting for a signal}
All the functionality below can be accessed with a single
@racket[require]:
@(defmodule unix-signals)
This library represents signal names as symbols all in upper-case;
for example, @racket['SIGUSR1] and @racket['SIGKILL].
@subsection{Waiting for a signal}
To receive Unix signals using this library, call To receive Unix signals using this library, call
@racket[capture-signal!] once for each signal of interest, and then @racket[capture-signal!] once for each signal of interest, and then
@ -56,15 +51,27 @@ Racket process.
@defproc[(capture-signal! [sig (or/c fixnum? symbol?)]) boolean?]{ @defproc[(capture-signal! [sig (or/c fixnum? symbol?)]) boolean?]{
Installs a signal handler for the given signal. When the given signal Installs a signal handler for the given signal. When the given signal
is received by the process, its signal number will be returned by uses is received by the process, its signal number will be returned by uses
of @racket[next-signal-evt] and/or @racket[read-signal]. } of @racket[next-signal-evt] and/or @racket[read-signal].
Note that this function is @|unsafe|:
it can corrupt or crash the Racket runtime system.
}
@defproc[(ignore-signal! [sig (or/c fixnum? symbol?)]) boolean?]{ @defproc[(ignore-signal! [sig (or/c fixnum? symbol?)]) boolean?]{
Causes the given signal to be ignored (@tt{SIG_IGN}) by the process. } Causes the given signal to be ignored (@tt{SIG_IGN}) by the process.
Note that this function is @|unsafe|:
it can corrupt or crash the Racket runtime system.
}
@defproc[(release-signal! [sig (or/c fixnum? symbol?)]) boolean?]{ @defproc[(release-signal! [sig (or/c fixnum? symbol?)]) boolean?]{
Installs the default handler (@tt{SIG_DFL}) for the given signal. } Installs the default handler (@tt{SIG_DFL}) for the given signal.
@defthing[next-signal-evt evt?]{ @tech[#:doc Note that this function is @|unsafe|:
it can corrupt or crash the Racket runtime system.
}
@defthing[next-signal-evt (evt/c fixnum?)]{ @tech[#:doc
'(lib "scribblings/reference/reference.scrbl")]{Synchronizable event} which '(lib "scribblings/reference/reference.scrbl")]{Synchronizable event} which
becomes ready when a signal previously registered with becomes ready when a signal previously registered with
@racket[capture-signal!] is received, at which point it returns the @racket[capture-signal!] is received, at which point it returns the
@ -77,17 +84,21 @@ number of the received signal. Signals are buffered internally using
the @link["http://cr.yp.to/docs/selfpipe.html"]{self-pipe trick}, and the @link["http://cr.yp.to/docs/selfpipe.html"]{self-pipe trick}, and
are therefore delivered in order of receipt. } are therefore delivered in order of receipt. }
@subsection{Sending a signal} @section{Sending a signal}
This library provides @racket[getpid] from @racketmodname[racket/os]
for convenience.
@defproc[(send-signal! [pid fixnum?] [sig (or/c fixnum? symbol?)]) @defproc[(send-signal! [pid fixnum?] [sig (or/c fixnum? symbol?)])
boolean?]{ Calls @tt{kill(2)} to deliver the given signal to the boolean?]{ Calls @tt{kill(2)} to deliver the given signal to the
given process ID. All special cases for @racket[pid] from the given process ID. All special cases for @racket[pid] from the
@tt{kill(2)} manpage apply. } @tt{kill(2)} manpage apply.
@subsection{Mapping between signal names and signal numbers} Note that this function is @|unsafe|:
it can corrupt or crash the Racket runtime system.
For convenience, this library also re-exports
@racket[getpid] from @racketmodname[racket/os].
}
@section{Mapping between signal names and signal numbers}
@defproc[(lookup-signal-number [sym symbol?]) (opt/c fixnum?)]{ @defproc[(lookup-signal-number [sym symbol?]) (opt/c fixnum?)]{
Returns a fixnum if the symbol name is defined, or @racket[#f] if not. } Returns a fixnum if the symbol name is defined, or @racket[#f] if not. }