Closed stephen-hawley closed 3 years ago
I added a few pushes to fix failing tests because I got impatient and didn't wait for the local tests to finish. Here's what this issues were: 1 - if a function didn't return anything but threw an exception, then we inject a new argument to return the medusa tuple for it. 2 - if a function is an extension, it has no instance, but it does have an injected argument 3 - index of instance needs to get bumped when a return argument gets injected And this is why I say "I hate you instant hole", which refers to an Acme product from a Roadrunner cartoon.
Swift allows closures to be declared as throwing. We need to support this.
This is not a complete PR. Here's why: Swift closures map into C# in one of 5 possible forms
For this PR, I tackled the last two. Doing all of these would make a PR that is ungainly(ier) and this one is going to be challenging enough. "If it was hard to write, it should be hard to read." In addition, I chose to only handle closures being passed from C# to swift, not the other way around (yet).
Quick refresher: if a function throws, it gets adapted with a magic tuple called a medusa tuple. The type in swift is
(T, Error, Bool)
where T is the return value of the function. This means that given the following function:We can't call this in C# because if it throws, the error is in R13 (or whatever register is available on the target platform) and we don't have access to that. It will get wrapped into a function like this, which is callable from C#:
Or something like that. Basically, if the Bool is true, then the Error part is valid else the T part is valid. It's called a medusa tuple because if you look at the wrong parts at the wrong time, you die.
Now, in order to handle closures we write an adapter in swift takes a C# callable closure and then adapts it into a swift closure that matches the original swift code. It calls the C# closure and either gets a result or gets an Error. It then either returns the result or throws the error.
Generally speaking, I broke more of the C# code that handles this up into this pattern:
So expect to see that pattern.
There is one other catch in all of this. When we are wrapping the C# function with a throwing closure, the C# type will be something like
Action<Tuple<T, SwiftError, Bool>, Tuple<args>>
and matching swift tuple will be this:(UnsafeMutablePointer<(T, Error, Bool)>, UnsafeMutablePointer<(args)>) -> ()
. Nowhere in there is there any information about whether the function throws, so our marshaler can't handle it. So I refactored it to include the original function so when we marshal arguments, we also pass in the original argument, which does have the information about throwing. To do this, I replaced the wrapper argument with the original function. The wrapper argument was unused, so this is a minor issue.Also, I found I bug in the swift code for getting a thrown exception that would crash if the alignment of the T return value is less than the pointer size. The trick is to get the offset to the Error without touching the first argument. The alignment of Bool is 1 so we crashed on access. That's fixed now. I looked into other solutions including addressing the tuple numerically:
let error = (T, Error, Bool).1
, but the generated code touches all the tuple elements, and not in a good way.Some lessons learned: Past Steve put in exceptions when the code hit closures that throw. Present Steve really appreciated that. It made the implementation much easier. Present Steve put those exceptions in for async so that future Steve won't suffer so much. Past Steve wrote routines to dump memory in C# and in Swift. Present Steve used them both and augmented the latter. It made debugging the crashes so much faster. Past Steve liked to use linq expressions for code. It can be very tight and pretty, but it's a royal pain to debug. I rewrote a couple of those in for loops which made fixing (inevitable) errors easier.