[swift-evolution] Swift null safety questions

Brent Royal-Gordon brent at architechies.com
Sat Mar 25 23:09:51 CDT 2017


> On Mar 23, 2017, at 9:01 AM, Joe Groff via swift-evolution <swift-evolution at swift.org> wrote:
> 
> setjmp and longjmp do *not* work well with Swift since they need compiler support to implement their semantics, and since Swift does not provide this support, setjmp-ing from Swift is undefined behavior. Empirical evidence that small examples appear to work is not a good way of evaluating UB, since any changes to Swift or LLVM optimizations may break it. Ease of implementation is also not a good criterion for designing things. *Supporting* a trap hook is not easy; it still has serious language semantics and runtime design issues, and may limit our ability to do something better.


Could we do something useful here without setjmp/longjmp? Suppose we had a function in the standard library that was roughly equivalent to this:

	typealias UnsafeFatalErrorCleanupHandler = () -> Void
	
	// Note the imaginary @_thread_local property behavior
	@_thread_local var _fatalErrorCleanupHandlers: [UnsafeFatalErrorCleanupHandler] = []
	
	func withUnsafeFatalErrorCleanupHandler<R>(_ handler: UnsafeFatalErrorCleanupHandler, do body: () throws -> R) rethrows -> R {
		_fatalErrorCleanupHandlers.append(handler)
		defer { _fatalErrorCleanupHandlers.removeLast() }
		return try body()
	}

And then, just before we trap, we do something like this:

	// Mutating it this way ensures that, if we reenter fatalError() in one of the handlers, 
	// it will pick up with the next handler.
	while let handler = _fatalErrorCleanupHandlers.popLast() {
		handler()
	}

I think that would allow frameworks to do something like:

	class Worker {
		let queue = DispatchQueue(label: "Worker")
		typealias RequestCompletion = (RequestStatus) -> Void

		enum RequestStatus {
			case responded
			case crashing
		}
		
		func beginRequest(from conn: IOHandle, then completion: RequestCompletion) {
			queue.async {
				withUnsafeFatalErrorCleanupHandler(fatalErrorHandler(completion)) {
					// Handle the request, potentially crashing
				}
			}
		}

		func fatalErrorHandler(_ completion: RequestCompletion) -> UnsafeFatalErrorCleanupHandler {
			return { completion(.crashing) }
		}
	}

	class Supervisor {
		let queue = DispatchQueue(label: "Supervisor")
		var workerPool: Pool<Worker>

		func startListening(to sockets: [IOHandle]) { … }
		func stopListening() { … }
		
		func bootReplacement() { … }
		
		func connected(by conn: IOHandle) {
			dispatchPrecondition(condition: .onQueue(queue))
			
			let worker = workerPool.reserve()
			
			worker.beginRequest(from: conn) { status in
				switch status {
				case .responded:
					conn.close()
					self.queue.sync {
						self.workerPool.relinquish(worker)
					}
				
				case .crashing:
					// Uh oh, we're in trouble.
					// 
					// This process is toast; it will not survive long beyond this stack frame.
					// We want to close our listening socket, start a replacement server, 
					// and then just try to hang on until the other workers have finished their 
					// current requests.
					// 
					// It'd be nice to send an error message and close the connection, 
					// but we shouldn't. We don't know what state the connection or the 
					// worker are in, so we don't want to do anything to them.We risk a 
					// `!==` check because it only involves a memory address stored in 
					// our closure context, not the actual object being referenced by it.
					
					// Go exclusive on the supervisor's queue so we don't try to handle 
					// two crashes at once (or a crash and something else, for that matter).
					self.queue.sync {
						self.stopListening()
						self.bootReplacement()
						
						// Try to keep the process alive until all of the surviving workers have 
						// finished their current requests. To do this, we'll perform barrier blocks 
						// on all of their queues, attached to a single dispatch group, and then 
						// wait for the group to complete.
						let group = DispatchGroup()
						
						for otherWorker in self.workerPool.all where otherWorker !== worker {
							// We run this as `.background` to try to let anything else the request might 
							// enqueue run first, and `.barrier` to make sure we're the only thing running.
							otherWorker.queue.async(group: group, qos: .background, flags: .barrier) {
								// Make sure we don't do anything else.
								otherWorker.queue.suspend()
							}
						}
						
						// We do not use `notify` because we need this stack frame to keep 
						// running so we don't trap yet.
						group.wait(timeout: .now() + .seconds(15))
					}
					
					// Okay, we can now return, and probably crash.
				}
			}
		}
	}

It's definitely not a full actor model, and you have to be very careful, but it might be a useful subset.

-- 
Brent Royal-Gordon
Architechies

-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.swift.org/pipermail/swift-evolution/attachments/20170325/81b76db2/attachment.html>


More information about the swift-evolution mailing list