RFC: declare public API#673
Conversation
|
CC @helje5 / @tanner0101 / @ianpartridge / @pushkarnk / @MrMage / @tachyonics / @kjessup / @normanmaurer / @Lukasa / @Joannis / @jrose-apple / @0xTim |
|
I don't quite follow, doesn't NIO 1 declare a public API? How is that different from what you're proposing? |
ktoso
left a comment
There was a problem hiding this comment.
Rules look good! Nice to specify more explicitly what is supported and what could get one into trouble.
Noticed some wording improvements, hope you don't mind :)
|
|
||
| ### What Are Acceptable Uses of NIO's Public API | ||
|
|
||
| - it's acceptable and expected to use any type and call any method from the `NIO*` modules unless the type or function name start with an underscore (`_`). |
There was a problem hiding this comment.
While we're here, can we capitalise the first words of all of these points? They're full sentences.
There was a problem hiding this comment.
I believe the initializers should be called out specifically. Since in case of an initializer the _ goes to the argument label (or parameter name). For example:
struct S {
public // not really
init(_internalWannabe: x: Whatever) { ... }
}At least these are the current rules for hiding symbols in SourceKit.
|
|
||
| Needless to say if you require at least version `2.3.4` you would specify `from: "2.3.4"`. The important part is that you use `from: "2.3.4"` or `.upToNextMajor(from: "2.3.4")` and not `.exact` or `.upToNextMinor`. | ||
|
|
||
| ### What Happens if You Ignore this Recommendation? |
There was a problem hiding this comment.
this Recommendation -> these Guidelines?
|
|
||
| The SwiftNIO project (which includes all of `github.com/apple/swift-nio*`)aims to follow [SemVer 2.0.0](https://semver.org/spec/v2.0.0.html) which requires to declare a public API. This document along with the [API documentation](https://apple.github.io/swift-nio/) declare SwiftNIO's API. | ||
|
|
||
| ### What Are Acceptable Uses of NIO's Public API |
There was a problem hiding this comment.
the - formatting is a bit hard to read to be honest; Since the new point is very close to the end of the examples/reasoning of previous one; Something to separate would be useful, be it a rule number or a --- ?
Or like:
Extensions to NIO types (level 3 or level 4)
what is allowed...
Why?
examples
|
|
||
| ### What Happens if You Ignore this Recommendation? | ||
|
|
||
| Hopefully, nothing at all ☺️. We have a source compatibility suite to which you can [ask to be added](https://forums.swift.org/t/register-as-swiftnio-user-to-get-ahead-of-time-security-notifications-be-added-to-the-source-compatibility-suite/17792) and we try not to break you (_within reason_). But it's impossible for us to test all of our users projects' and we don't want to lose the ability to move fast without breaking things. Certain failures like clashing protocol conformances can have delicate failure modes. |
There was a problem hiding this comment.
While that's true and cool, the first sentence sounds a bit too soft to me... Perhaps start re stating that ignoring those rules may cause you to have to rewrite things etc, and then mention the happy news that there is a compat suite so you'd get heads up (but still MAY need to change things if team decides to have to change such internal API etc)
| - ✅ `channel.close(promise: nil)` | ||
| - ❌ `channel._unsafe.flush0()`, underscored property | ||
| - ❌ `import CNIOAtomics`, module name doesn't start with NIO | ||
| - it is acceptable to conform NIO types to protocols _you control_, ie. a protocol in _your codebase_. It's not acceptable to confirm any NIO types to protocols in the standard library or outside of your control. |
|
@ianpartridge well, we don't have a document specifying what we consider public API which makes SemVer unclear. We have always operated under pretty much the principles states in this doc (except that we introduced new types not prefixed with Without declaring what exactly we consider public API, one might interpret SemVer the way that anything that might break any potentially existing project is SemVer major.
With this document we hope to clarify what additions to our APIs we consider SemVer minor. But really nothing changes :) |
|
OK thanks. I think this is valuable for the SPM ecosystem in general and it would be good if some of this could go "upstream" to there :) |
| @@ -0,0 +1,65 @@ | |||
| # SwiftNIO 2 Public API | |||
|
|
|||
| The SwiftNIO project (which includes all of `github.com/apple/swift-nio*`)aims to follow [SemVer 2.0.0](https://semver.org/spec/v2.0.0.html) which requires to declare a public API. This document along with the [API documentation](https://apple.github.io/swift-nio/) declare SwiftNIO's API. | |||
There was a problem hiding this comment.
Nit: space after closing parenthesis.
| @@ -0,0 +1,65 @@ | |||
| # SwiftNIO 2 Public API | |||
|
|
|||
| The SwiftNIO project (which includes all of `github.com/apple/swift-nio*`)aims to follow [SemVer 2.0.0](https://semver.org/spec/v2.0.0.html) which requires to declare a public API. This document along with the [API documentation](https://apple.github.io/swift-nio/) declare SwiftNIO's API. | |||
There was a problem hiding this comment.
Nit: replace "requires to" with "requires projects to".
| - ✅ `channel.close(promise: nil)` | ||
| - ❌ `channel._unsafe.flush0()`, underscored property | ||
| - ❌ `import CNIOAtomics`, module name doesn't start with NIO | ||
| - it is acceptable to conform NIO types to protocols _you control_, ie. a protocol in _your codebase_. It's not acceptable to confirm any NIO types to protocols in the standard library or outside of your control. |
There was a problem hiding this comment.
Blank line above this please.
|
|
||
| ### What Are Acceptable Uses of NIO's Public API | ||
|
|
||
| - it's acceptable and expected to use any type and call any method from the `NIO*` modules unless the type or function name start with an underscore (`_`). |
There was a problem hiding this comment.
While we're here, can we capitalise the first words of all of these points? They're full sentences.
| Examples: | ||
| - ✅ `extension EventLoopFuture: MyOwnProtocol { ... }`, assuming `MyOwnProtocol` lives in your codebase | ||
| - ❌ `extension EventLoopFuture: DebugStringConvertible { ... }`, `DebugStringConvertible` is a standard library protocol | ||
| - it is acceptable to conform _your own types_ to NIO protocols but it's not acceptable to conform types you do not control to NIO protocols. |
There was a problem hiding this comment.
A blank line above this please.
| - ✅ `extension MyHandler: ChannelHandler { ... }` | ||
| - ❌ `extension Array: EventLoopGroup where Element: EventLoop { ... }`, as `Array` lives in the standard library | ||
|
|
||
| - it is acceptable to add extensions to NIO types that either use one of your types as a non-default argument or are prefixed in a way that prevents clashes |
|
|
||
| ### What's the Point of Specifying All This? | ||
|
|
||
| We believe that it's important for the ecosystem that all parties can adopt each others new versions quickly and easily. Therefore we always strongly recommend to depend on all the projects in the NIO family which have a `major version >= 1` up to the next _major_ version. By following the rules set out above we should all not run into any trouble even when run with newer versions. |
|
|
||
| ### What's the Point of Specifying All This? | ||
|
|
||
| We believe that it's important for the ecosystem that all parties can adopt each others new versions quickly and easily. Therefore we always strongly recommend to depend on all the projects in the NIO family which have a `major version >= 1` up to the next _major_ version. By following the rules set out above we should all not run into any trouble even when run with newer versions. |
There was a problem hiding this comment.
Nit: replace "strongly recommend to depend" with "strongly recommend depending"
|
|
||
| Needless to say if you require at least version `2.3.4` you would specify `from: "2.3.4"`. The important part is that you use `from: "2.3.4"` or `.upToNextMajor(from: "2.3.4")` and not `.exact` or `.upToNextMinor`. | ||
|
|
||
| ### What Happens if You Ignore this Recommendation? |
|
|
||
| ### What Happens if You Ignore this Recommendation? | ||
|
|
||
| Hopefully, nothing at all ☺️. We have a source compatibility suite to which you can [ask to be added](https://forums.swift.org/t/register-as-swiftnio-user-to-get-ahead-of-time-security-notifications-be-added-to-the-source-compatibility-suite/17792) and we try not to break you (_within reason_). But it's impossible for us to test all of our users projects' and we don't want to lose the ability to move fast without breaking things. Certain failures like clashing protocol conformances can have delicate failure modes. |
There was a problem hiding this comment.
Italics and a parenthetical? How dramatic! I'd recommend either only the italics or only the parenthetical.
jrose-apple
left a comment
There was a problem hiding this comment.
It's nice to have something like this written up, especially in a language that allows overloading and type inference. It goes with the discussions that have been happening on the forums too.
| - ✅ `extension MyHandler: ChannelHandler { ... }` | ||
| - ❌ `extension Array: EventLoopGroup where Element: EventLoop { ... }`, as `Array` lives in the standard library | ||
|
|
||
| - it is acceptable to add extensions to NIO types that either use one of your types as a non-default argument or are prefixed in a way that prevents clashes |
There was a problem hiding this comment.
This one seems phrased a little on the restrictive side to me. Unlike in Objective-C, there's no run-time conflict when this rule is broken, and eventually we'll have a way to disambiguate.
If you wanted to split the difference, maybe this should be qualified to "public API"? Helpers within a module are generally acceptable (though I should double-check that we actually prefer them in tie-breakers).
The condition you're using may not be sufficient to cover everything, either, depending on how you phrase it. I could have a parameter of type Array<MyType>, but [] wouldn't uniquely identify it.
There was a problem hiding this comment.
@jrose-apple thank you! This was meant to be public extensions. This demonstrates the clash I mean:
(everything below a // MODULE: xyz would be in that module)
import BadModule
import RandomClient
doIt()
doItBad()
// MODULE: NIO
public struct ByteBuffer {
public init() {
}
public func foo() {
print("NIO.foo")
}
}
// MODULE: BadModule
import NIO
public extension ByteBuffer {
func foo() {
print("BadModule.foo")
}
}
public func doItBad() {
ByteBuffer().foo()
}
// MODULE: RandomClient
import NIO
import BadModule
public func doIt() {
ByteBuffer().foo()
}
which would get you
Compile Swift Module 'NIO' (1 sources)
Compile Swift Module 'BadModule' (1 sources)
Compile Swift Module 'RandomClient' (1 sources)
/private/tmp/.gen-swift-perf-test_vUBGJT/Sources/RandomClient/module.swift:35:1: error: ambiguous use of 'foo()'
ByteBuffer().foo()
^
NIO.ByteBuffer:3:17: note: found this candidate
public func foo()
^
BadModule.ByteBuffer:2:17: note: found this candidate
public func foo()
^
internal helpers are always fine here and they would be preferred which is what we want 👍
There was a problem hiding this comment.
Sorry, minor typo drive-by: s/confirm/conform/
| - we might add a new module called `NIOHelpers` | ||
| - we will not add a new module called `Helpers` | ||
| - we might add a new global functions called `nioRun` (very unlikely) or `c_nio_SSL_write` (more likely) | ||
| - we will not add a new global function called `SSL_write` |
There was a problem hiding this comment.
Hm. But what if SwiftNIO grows a new exported dependency?
There was a problem hiding this comment.
if we vendor new stuff, we symbol-prefix everything:
- https://github.com/apple/swift-nio/blob/master/Sources/CNIOHTTPParser/update_and_patch_http_parser.sh#L48-L65
- https://github.com/apple/swift-nio/blob/master/Sources/CNIOSHA1/update_and_patch_sha1.sh#L43
- https://forums.swift.org/t/rfc-moving-swiftnio-ssl-to-boringssl/18280
you're right thought that if we added a new shared library dependency this might be an issue. I guess we should not do that...
But the same issue exists for all shared libraries we or Swift itself depends on.
@Lukasa / @normanmaurer should we do any guarantees here? I'd be fine to say that we won't add any further shared library dependencies without a new major version.
There was a problem hiding this comment.
What if your dependencies grow a new exported dependency?
(I'm trying to reductio ad absurdum here. Swift's namespacing model just doesn't allow this guarantee in practice.)
There was a problem hiding this comment.
@jrose-apple thank you, this is very valuable! So what I'm trying to say (and I'll try to rephrase this) is that we won't add any C symbols into our binaries. We have done this (http_parser.c wasn't name-spaced in NIO 1.0.0) and it lead to problems because people combined NIO with something else that also vendored a non-name-spaced http_parser.c.
I understand that Swift's namespacing model doesn't allow those guarantees but so far Swift mangles enough information into the symbol names that we didn't suffer any of those clashes in reality. For the vendored C stuff however this was totally different and our users have actually hit this which is why we symbol-prefix everything C now.
We're also in a pretty unique spot with NIO that we don't have many dependencies at all. NIO core dynamically links the Swift stdlib, runtime, etc (not Foundation), zlib, libc and nothing else and we're not changing this. The rule has always been: Anything that depends on other system libraries goes into a separate package (therefore swift-nio-ssl and swift-nio-transport-services). And I don't think swift-nio-ssl/transport-services should grow a new dependency without a fresh major version. All of our packages so far only depend on only NIO stuff.
I believe this should basically get us to a place where if NIO 2.x.m works fine, the user types swift package update gets 2.y.n (where y >= x) and there's no way it doesn't work (unless the user has their own modules/C functions with CNIO,c_nio,NIO, ... prefixes and we added one that clashed). Do you think this is accurate?
$ nm ./.build/x86_64-apple-macosx10.10/debug/NIOTLSServer | grep ' T ' | grep -v -e NIO -e catmc -e c_nio
0000000100000000 T __mh_execute_header
00000001001a8010 T _main
$ nm ./.build/x86_64-apple-macosx10.10/debug/NIOHTTP1Server | grep ' T ' | grep -v -e NIO -e catmc -e c_nio
0000000100000000 T __mh_execute_header
00000001001aecc0 T _main
(catmc is for historical reasons the prefix for our C atomics but they have been there since 1.0.0 so nothing got added and we should really rename them to c_nio_)
$ nm ./.build/x86_64-apple-macosx10.10/debug/NIOHTTP1Server | grep -i ' U ' | grep -v -e ' _swift' -e ' _$S' -e ' _pthread' -e ' _obj' -e ' _OBJ' -e ' _dispatch' -e ' __swift' -e ' _class' -e ' _CF' -e ' __objc' -e ' __Block' -e ' _protocol' -e ' _proper'
U __DefaultRuneLocale
U __NSConcreteStackBlock
U ___assert_rtn
U ___error
U ___memmove_chk
U ___memset_chk
U ___stack_chk_fail
U ___stack_chk_guard
U __dyld_register_func_for_add_image
U __stdlib_malloc_size
U _abort
U _accept
U _asprintf
U _bind
U _bzero
U _calloc
U _close
U _connect
U _deflate
U _deflateBound
U _deflateEnd
U _deflateInit2_
U _dup
U _errx
U _free
U _freeaddrinfo
U _freeifaddrs
U _getaddrinfo
U _getifaddrs
U _getpeername
U _getpid
U _getsockname
U _getsockopt
U _gettimeofday
U _hash_create
U _hash_search
U _if_nametoindex
U _inet_ntop
U _inet_pton
U _ivar_getName
U _ivar_getOffset
U _kCFCoreFoundationVersionNumber
U _kevent
U _kqueue
U _listen
U _log2
U _lseek
U _malloc
U _memchr
U _memcmp
U _memcpy
U _memset
U _method_setImplementation
U _read
U _realloc
U _recvfrom
U _sel_getUid
U _sendfile
U _sendmsg
U _sendto
U _setsockopt
U _shutdown
U _socket
U _strcmp
U _strerror
U _strlen
U _strncmp
U _sysconf
U _write
U _writev
U dyld_stub_binder
There was a problem hiding this comment.
I guess it's just as possible that zlib adds an unprefixed function, but because you control your dependencies so tightly you might be able to get away with this.
|
|
||
| ### What Happens if You Ignore this Recommendation? | ||
|
|
||
| Hopefully, nothing at all ☺️. We have a source compatibility suite to which you can [ask to be added](https://forums.swift.org/t/register-as-swiftnio-user-to-get-ahead-of-time-security-notifications-be-added-to-the-source-compatibility-suite/17792) and we try not to break you (_within reason_). But it's impossible for us to test all of our users projects' and we don't want to lose the ability to move fast without breaking things. Certain failures like clashing protocol conformances can have delicate failure modes. |
There was a problem hiding this comment.
Nitpick: our users' projects – I believe the apostrophe is attached to a wrong word.
8dae9e3 to
eb86820
Compare
|
thank you @moiseev, @jrose-apple, @Lukasa and @ktoso , took all your comments. Let me know what you think about v2 :) |
|
|
||
| ## What are acceptable uses of NIO's Public API | ||
|
|
||
| ### Type, methods and modules |
There was a problem hiding this comment.
SSWG documents make use of serial comma, so it might make sense to follow suit in this document as well for consistency.
|
|
||
| ## What happens if you ignore these guidelines? | ||
|
|
||
| We are trying to be nice people and we ❤️ our users so we will never break anybody just because they didn't perfectly stick to our guidelines. But just ignoring those guidelines might mean rewriting some of your code, debugging random build or runtime failures that we didn't hit in the pre-release testing. We do have a source compatibility suite to which you can [ask to be added](https://forums.swift.org/t/register-as-swiftnio-user-to-get-ahead-of-time-security-notifications-be-added-to-the-source-compatibility-suite/17792) and we try not to break you (_within reason_). But it is impossible for us to test all of our users' projects and we don't want to lose the ability to move fast without breaking things. Certain failures like clashing protocol conformances can have delicate failure modes. No newline at end of file |
There was a problem hiding this comment.
those guidelines
Should probably say these like the title does.
|
|
||
| ### Type, methods and modules | ||
|
|
||
| It is acceptable and expected to use any type and call any method from the `NIO*` modules unless the type or function name start with an underscore (`_`). For `init`s this includes the first parameter's name. |
There was a problem hiding this comment.
Should this specify explicitly "public" or "open" methods and types? For instance, using @testable import you could possibly get access to unstable methods not prefixed with _.
There was a problem hiding this comment.
Maybe we should say "exported"?
|
|
||
| ### Conforming NIO Types to Protocols | ||
|
|
||
| It is acceptable to conform NIO types to protocols _you control_, ie. a protocol in _your codebase_. It is not acceptable to conform any NIO types to protocols in the standard library or outside of your control. |
|
|
||
| ##### What's the point of this restriction? | ||
|
|
||
| NIO in a later new minor version might conform its type to said protocol. |
There was a problem hiding this comment.
We may want to say here that NIO may conform its type to said protocol, or the package that provides the protocol may conform the NIO type to their protocol.
|
|
||
| ##### What's the point of this restriction? | ||
|
|
||
| Again, NIO might later add this conformance. |
There was a problem hiding this comment.
And here, maybe say that NIO might add this conformance or the owner of the type may add the conformance.
|
|
||
| ## What's the point of codifying all this? | ||
|
|
||
| We believe that it is important for the ecosystem that all parties can adopt each others' new versions quickly and easily. Therefore we always strongly recommend depending on all the projects in the NIO family which have a `major version >= 1` up to the next _major_ version. By following the guidelines set out above we should all not run into any trouble even when run with newer versions. |
There was a problem hiding this comment.
Do we need the backticks here? Should we just say "a major version greater than 1"?
|
|
||
| ## What happens if you ignore these guidelines? | ||
|
|
||
| We are trying to be nice people and we ❤️ our users so we will never break anybody just because they didn't perfectly stick to our guidelines. But just ignoring those guidelines might mean rewriting some of your code, debugging random build or runtime failures that we didn't hit in the pre-release testing. We do have a source compatibility suite to which you can [ask to be added](https://forums.swift.org/t/register-as-swiftnio-user-to-get-ahead-of-time-security-notifications-be-added-to-the-source-compatibility-suite/17792) and we try not to break you (_within reason_). But it is impossible for us to test all of our users' projects and we don't want to lose the ability to move fast without breaking things. Certain failures like clashing protocol conformances can have delicate failure modes. No newline at end of file |
There was a problem hiding this comment.
I still think the parentheses and the italics is excessive. :D
a28592d to
6ec5efd
Compare
| - ✅ We might add a new global type called `NIOJetPack`. | ||
| - ✅ We might add a new module called `NIOHelpers`. | ||
| - ✅ We might add a new global functions called `nioRun` (very unlikely) or `c_nio_SSL_write` (more likely). | ||
| - ❌ We will not add a global new type called `Tomato`. |
There was a problem hiding this comment.
As discussed with @weissi OOB, I'm not sure that this is necessary. Shadowing should work here too, no? (i.e. the new type doesn't introduce compiler amgiguity?) (this specific section lacks the "What's the point of this restriction?" :-)
This is slightly off-topic, but IMO all stuff should either be prefixed NIO or not, for API consistency. When looking at the sidebar of https://apple.github.io/swift-nio/docs/current/NIO/index.html those NIO types immediately hit your eye as outliers.
Right now NIO mostly gets away w/o prefixes because it just renamed all types Foundation already has (EventLoop instead of RunLoop etc, i.e. it used the Java terminology). I would personally either continue doing this (and e.g. event new names for NIOAny, maybe ChannelBucket?), or prefix everything for consistency and ideally rename stuff to match Foundation (e.g. NIORunLoop instead of EventLoop).
There was a problem hiding this comment.
@helje5 let's assume three modules:
NIOwhich in version 2.0 does not declare astruct TomatoCapresewhich declares apublic struct TomatoMyApp
Let's also assume that MyApp has both import Caprese and import NIO and it uses Tomato (implicitly from Caprese for example like this):
let t = Tomato()
If now in version NIO in version 2.1.0 introduces a public struct Tomato { ... } as well, then now compilation of MyApp will fail will an ambiguity error.
The only way to resolve it is that NIO introduces it as NIOTomato and everything is good. Does that make sense?
There was a problem hiding this comment.
btw, event loop is also not "Java terminology" it's just a standard term for a mechanism that dispatches events into a program. Run loop is less specific.
There was a problem hiding this comment.
Makes sense, maybe you want to add the explanation to the document (like you did in the other sections).
(P.S.: I wanted to say "Netty terminology", but that is nitpicking. I think the point in itself is valid).
|
|
||
| #### Why is this useful to you? | ||
|
|
||
| Let's assume your application is composed of three modules: |
|
is there a reason why this isn't scheduled for the 2.0.0 Milestone? |
|
@Lukasa what do you think. Is this good to go as a 1st pass? |
Motivation: SemVer requires us to declare a public API, we should do so for NIO 2. Modifications: declare our public API. Result: more transparency into our versioning policy.
Motivation:
SemVer requires us to declare a public API, we should do so for NIO 2.
Modifications:
declare our public API.
Result: