Hi,
Imagine the following simple "algorithm" (or strategy): "shadowed values always have priority, otherwise the current value of global variables are used." Problem is that, how could the lambda function know it was called with a shadowed value? An easy way is to use another global variable which should be never changed by SETQ:
So consider the following modified examples:
(defvar *foo* "original value") (defvar *shadowed* nil)
(defun create-server (&optional (port 1965)) (usocket:socket-server "0.0.0.0" port (let ((foo *foo*) (shadowed *shadowed*)) (lambda (stream) (let ((v (if shadowed foo *foo*))) (write v :stream stream)))) () :multi-threading t :element-type 'character :in-new-thread t))
(defparameter *server1* (let ((*foo* "shadowed value") (*shadowed* t)) (create-server 1965)))
(defparameter *server2* (create-server 1966))
There are two "servers" listening on port 1965 and 1966. Now I have:
$ telnet 127.0.0.1 1965 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. "shadowed value"
$ telnet 127.0.0.1 1966 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. "original value"
After I changed the value of *foo* by (SETQ *FOO* "new value"):
$ telnet 127.0.0.1 1965 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. "shadowed value"
$ telnet 127.0.0.1 1966 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. "new value"
But note that, in general, if you change the value of a global variable from one thread, the change may not be immediately visible from another thread, unless you use something like locks (or atomic updates) from multi-threading libraries. The related issue is not in scope of the socket library.
Regards,
Chun Tian
On Dec 31, 2021, at 05:48, Duncan Bayne duncan@bayne.id.au wrote:
Chun Tian writes:
The difference here is that now (lambda (stream) ...) is a closure which will contain a local version of *foo* at the time when (create-server) is called. This kind of uses of lambda functions is like a cheap object with a member variable.
Thanks for the suggestion - yes, this does work as expected, but introduces a difficulty with the API of the library.
The germinal code is as follows (edited to remove large swathes and just focus on the relevant bits):
===== (defvar *germinal-cert* "/etc/germinal/cert.pem") (defvar *germinal-cert-key* "/etc/germinal/key.pem") (defvar *germinal-tls-context* nil "Variable used to store global TLS context")
;; snip
(with-global-context (*germinal-tls-context* :auto-free-p (not background)) (usocket:socket-server host port #'gemini-handler () :multi-threading t :element-type '(unsigned-byte 8) :in-new-thread background)))
;; snip
(defun gemini-handler (stream) "The main Gemini request handler. Sets up TLS and sets up request and response" (handler-case (let* ((tls-stream (make-ssl-server-stream stream :certificate *germinal-cert* :key *germinal-cert-key*)) ;; snip =====
So replacing the handler function with a lambda that creates a closure works ... but breaks the non-testing case where you just want to setq the special variables in your app startup and be done with it.
The best approach I can think of is something like ...
===== ;; snip (with-global-context (*germinal-tls-context* :auto-free-p (not background)) (usocket:socket-server host port (let ((*threaded-cert* *germinal-cert*) (*threaded-cert-key* *germinal-cert-key*)) (lambda (stream) (gemini-handler stream))) ;; snip (let* ((tls-stream (make-ssl-server-stream stream :certificate *threaded-cert* :key *threaded-cert-key*)) ;; snip =====
Which seems weird, but also gives the best of both worlds; the ability to shadow variables for testing purposes, but also setq the *same* variables for global configuration.
Thoughts / opinions?
-- Duncan Bayne +61 420 817 082 | https://duncan.bayne.id.au/
I usually check my mail every 24 - 48 hours. If there's something urgent going on, please send me an SMS or call me.