Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RFC: declare public API #673

Merged
merged 2 commits into from
Jan 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ SwiftNIO aims to support all of the platforms where Swift is supported. Currentl

The latest released SwiftNIO version supports Swift 4.0, 4.1, and 4.2.

### Compatibility

SwiftNIO follows [SemVer 2.0.0](https://semver.org/#semantic-versioning-200) with a separate document declaring [SwiftNIO's Public API](docs/public-api.md).

What this means for you is that you should depend on SwiftNIO with a version range that covers everything from the minimum SwiftNIO version you require up to the next major version.
In SwiftPM that can be easily done specifying for example `from: "2.0.0"` meaning that you support SwiftNIO in every version starting from 2.0.0 up to (excluding) 3.0.0.
SemVer and SwiftNIO's Public API guarantees should result in a working program without having to worry about testing every single version for compatibility.

### Basic Architecture

The basic building blocks of SwiftNIO are the following 8 types of objects:
Expand Down
117 changes: 117 additions & 0 deletions docs/public-api.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# 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 projects 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

### 1. Using NIO types, methods, and modules

It is acceptable and expected to use any exported type and call any exported 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.

##### What's the point of this restriction?

If we prefix something with an underscore or put it into one of NIO's internal modules, we can't commit to an API for it and don't expect users to ever use this. Also, there might be different underscored functions/types depending on the platform.

##### Examples

- ✅ `channel.close(promise: nil)`
- ❌ `channel._unsafe.flush0()`, underscored property
- ❌ `import CNIOAtomics`, module name doesn't start with NIO
- ❌ `ByteBuffer(_enableSuperSpecialAllocationMode: true)`, as the initialiser's first argument is underscored

---

### 2. Conforming NIO types to protocols

It is acceptable to conform NIO types to protocols _you control_, i.e. 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 version may conform its type to said protocol, or the package that provides the protocol may conform the NIO type to _their_ protocol.

##### Examples

- ✅ `extension EventLoopFuture: MyOwnProtocol { ... }`, assuming `MyOwnProtocol` lives in your codebase
- ❌ `extension EventLoopFuture: DebugStringConvertible { ... }`, `DebugStringConvertible` is a standard library protocol

---

### 3. Conforming your types to NIO protocols

It is acceptable to conform _your own types_ to NIO protocols but it is not acceptable to conform types you do not control to NIO protocols.

##### What's the point of this restriction?

NIO in a later version might add this conformance or the owner of the type may add the conformance.

##### Examples

- ✅ `extension MyHandler: ChannelHandler { ... }`
- ❌ `extension Array: EventLoopGroup where Element: EventLoop { ... }`, as `Array` lives in the standard library

---

### 4. Extending NIO types

It is acceptable to extend NIO types with `public` methods/properties that either use one of your types as a non-default argument or are prefixed in a way that prevents clashes. Adding `private`/`internal` methods/properties is always acceptable.

#### What's the point of these restrictions?

NIO might later add a member function/property with the same signature as yours.

##### Examples

- ✅ `extension ByteBuffer { public mutating func readMyType(at: Int) -> MyType {...} }`, acceptable because it returns a `MyType`
- ✅ `extension ByteBuffer { public mutating func myProjectReadInteger(at: Int) -> Int {...} } `, acceptable because prefixed with `myProject`
- ✅ `extension ByteBuffer { internal mutating func readFloat(at: Int) -> Float {...} }`, acceptable even though `Float` is not in your control because the function is `internal`
- ❌ `extension ByteBuffer { public mutating func readBool(at: Int) -> Bool {...} }`, because `Bool` is a standard library type not in your control


## Promises the NIO team make

Before releasing a new major version, i.e. SwiftNIO 3.0.0 we promise the following:

### 1. No global namespace additions that aren't prefixed

In a minor or patch version, all global types/modules/functions that we will add will have `NIO*`/`nio*`/`CNIO*`/`cnio*`/`c_nio*` prefixes, this includes C symbols that are visible to others. Be aware that we don't control the exact versions of the system libraries installed on your system so we can't make any guarantees about the symbols exported by the system libraries we depend on.


##### What does this mean concretely?

- ✅ 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`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@helje5 let's assume three modules:

  1. NIO which in version 2.0 does not declare a struct Tomato
  2. Caprese which declares a public struct Tomato
  3. MyApp

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?

Copy link
Member Author

@weissi weissi Dec 5, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@helje5 added a paragraph, useful?

- ❌ We will not add a new module called `Helpers`.
- ❌ We will not add a new global function called `SSL_write`.
- ❌ We will not add a new system library dependency without a new major version.

#### Why is this useful to you?

Let's assume your application is composed of three modules:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@helje5 this is what I added.


1. `NIO`, which in version 2.0.0 does not declare a `public struct Tomato`
2. `Caprese`, which declares a `public struct Tomato`
3. `MyApp`

Let's also assume that `MyApp` has both `import Caprese` and `import NIO` and it uses `Tomato` (from `Caprese`). For example `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`'s compilation will fail will an ambiguity error.

With the guarantee that in minor versions NIO will only introduce `NIO`-prefixed types, this can not happen because NIO would call its type `NIOTomato` rather than `Tomato`.

C functions are all in a global namespace so even worse issues might hit you if NIO were to just introduce exported C functions with very general names in minor releases.

## 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 greater than or equal to 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.

Example:

.package(url: "https://github.com/apple/swift-nio.git", from: "2.0.0"),

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 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 these 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.