Zero-cost 'Service Provider Interface'/Signature Packages


(Johannes Weiss) #1

Hi swift-dev,

I talked to a few people about this problem and we agreed that it is a problem and that it needs to be discussed. I didn't quite know where it would fit best but let's go with swift-dev, please feel free to tell to post it elsewhere if necessary. And apologies for the long mail, couldn't come up with a sensible tl;dr...

Let me briefly introduce the problem what for the lack of a better name I call 'signature package' or 'Service Provider Interface' (SPI) as some people from the Java community seem to be calling it (https://en.wikipedia.org/wiki/Service_provider_interface). For the rest of this email I'll use the term SPI.

In a large ecosystem there is a few pieces that many libraries will depend on and yet it seems pretty much impossible to standardise exactly one implementation. Logging is a very good example as many people have different ideas about how logging should and should not work. At the moment I guess your best bet is to use your preferred logging API and hope that all your other dependencies use the same one. If not you'll likely run into annoying problems (different sub-systems logging to different places or worse).

Also, in a world where some dependencies might be closed source this is an even bigger problem as clearly no open-source framework will depend on something that's not open-source.

In Java the way seems to be to standardise on some logging interface (read `protocol`) with different implementations. For logging that'd probably be SLF4J [4]. In Swift:

    let logger: LoggerProtocol = MyFavouriteLoggingFramework(configuration)

where `LoggerProtocol` comes from some SPI package and `MyFavouriteLoggingFramework` is basically what the name says. And as a general practise, everybody would only use `LoggerProtocol`. Then tomorrow when I'll change my mind replacing `MyFavouriteLoggingFramework` by `BetterFasterLoggingFramework` does the job. With 'dependency injection' this 'logger' is handed through the whole program and there's a good chance of it all working out. The benefits are that everybody just needs to agree on a `protocol` instead of an implementation. :+1:

In Swift the downside is that this means we're now getting a virtual dispatch and the existential everywhere (which in Java will be optimised away by the JIT). That might not be a huge problem but it might undermine 'CrazyFastLoggingFramework's adoption as we always pay overhead.

I don't think this problem can be elegantly solved today. What I could make work today (and maybe we could add language/SwiftPM support to facilitate it) is this (:warning:ÔłŹ, it's ugly)

- one SwiftPM package defines the SPI only, the only thing it exports is a `public protocol` called say `_spi_Logger`, no implementation
- every implementation of that SPI defines a `public struct Logger: _spi_Logger` (yes, they all share the _same_ name)
- every package that wants to log contains

    #if USE_FOO_LOGGER
        import FooLogger
    #elif USE_BAR_LOGGER
        import BarLogger
    #else
        import BuzLogger
    #endif

  where 'BuzLogger' is the preferred logging system of this package but if either `USE_FOO_LOGGER` or `USE_BAR_LOGGER` was defined this package is happy to use those as well.
- `Logger` is always used as the type, it might be provided by different packages though
- in Package.swift of said package we'll need to define something like this:

     func loggingDependency() -> Package.Dependency {
     #if USE_FOO_LOGGER
         return .package(url: "github.com/...../foo.git", ...)
     #elif USE_BAR_LOGGER
         return ...
     #else
         return .package(url: "github.com/...../buz.git", ...)
     #endif
     }

      func loggingDependencyTarget() -> Target.Dependency {
     #if USE_FOO_LOGGER
         return "foo"
     #elif USE_BAR_LOGGER
         return "bar"
     #else
         return "buz"
     #endif
     }
- in the dependencies array of Package.swift we'll then use `loggingDependency()` and in the target we use `loggingDependencyTarget()` instead of the concrete one

Yes, it's awful but even in a world with different opinions about the implementation of a logger, we can make the program work.
In the happy case where application and all dependency agree that 'AwesomeLogging' is the best framework we can just type `swift build` and everything works. In the case where some dependencies think 'AwesomeLogging' is the best but others prefer 'BestEverLogging' we can force the whole application into one using `swift build -Xswiftc -DUSE_AWESOME_LOGGING` or `swift build -Xswiftc -DUSE_BEST_EVER_LOGGING`.

Wrapping up, I can see a few different options:

1) do nothing and live with the situation (no Swift/SwiftPM changes required)
2) advertise something similar to what I propose above (no Swift/SwiftPM changes required)
3) do what Java does but optimise the existential away at compile time (if the compiler can prove there's actually only one type that implements that protocol)
4) teach SwiftPM about those SPI packages and make everything work, maybe by textually replacing the import statements in the source?
5) do what Haskell did and retrofit a module system that can support this
6) have 'special' `specialized protocol` for which a concrete implementation needs to be selected by the primary source
7) something I haven't thought of

Btw, both Haskell (with the new 'backpack' [1, 2]) and ML have 'signatures' to solve this problem. A signature is basically an SPI. For an example see the backpack-str [3] module in Haskell which defines the signature (str-sig) and a bunch of different implementations for that signature (str-bytestring, str-string, str-foundation, str-text, ...).

Let me know what you think!

[1]: https://plv.mpi-sws.org/backpack/
[2]: https://ghc.haskell.org/trac/ghc/wiki/Backpack
[3]: https://github.com/haskell-backpack/backpack-str
[4]: https://www.slf4j.org

-- Johannes
PS: I attached a tar ball which contains the following 6 SwiftPM packages that are created like I describe above:

- app, the main application, prefers the 'foo' logging library
- somelibA, some library which logs and prefers the 'foo' logging library
- somelibB, some other library which prefers the 'bar' logging library
- foo, the 'foo' logging library
- bar, the 'bar' logging library
- spi, the logging SPI

The dependency default graph looks like this:
      +- somelibA ---+ foo
     / / \
app +--------------/ +-- spi
     \ /
      +- somelibB ---- bar

that looks all good, except that 'foo' and 'bar' are two logging libraries :see_no_evil:. In other words, we're in the unhappy case, therefore just typing `swift build` gives this:

--- SNIP ---
-1- johannes:~/devel/swift-spi-demo/app
$ swift build
Compile Swift Module 'app' (1 sources)
/Users/johannes/devel/swift-spi-demo/app/Sources/app/main.swift:14:23: error: cannot convert value of type 'Logger' to expected argument type 'Logger'
somelibB_func(logger: logger)
                      ^~~~~~
error: terminated(1): /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-build-tool -f /Users/johannes/devel/swift-spi-demo/app/.build/debug.yaml main
--- SNAP ---

because there's two `Logger` types. But selecting `foo` gives (note that all lines start with 'Foo:'):

--- SNIP ---
$ swift build -Xswiftc -DUSE_FOO
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Foo: info: hello from the app
Foo: info: hello from somelibA
Foo: info: hello from somelibB
Foo: info: hello from somelibA
Foo: info: hello from somelibB
--- SNAP ---

and for 'bar' (note that all lines start with 'Bar:')

--- SNIP ---
$ swift build -Xswiftc -DUSE_BAR
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Bar: info: hello from the app
Bar: info: hello from somelibA
Bar: info: hello from somelibB
Bar: info: hello from somelibA
Bar: info: hello from somelibB
--- SNAP ---

swift-spi-demo.tar.gz (62.9 KB)


Logging
(Daniel Dunbar) #2

My personal preference is to:
1. Do nothing for now, but encourage publishing standardized protocols to solve this need.
2. Hope for a future with WMO+LTO magic which recovers the performance, for the case where the entire application ends up using one implementation.

You can manage some of the ‚Äúdependency injection‚ÄĚ part by making the package which exports the common protocol also export a global variable for the concrete implementation in use (with setters). That could just be a ‚Äúpattern‚ÄĚ people follow. This wouldn‚Äôt be particularly pretty, but it would mean that intermediate packages could avoid declaring a concrete dependency on any one implementation, and leave it up to clients to pick.

- Daniel

···

On Nov 2, 2017, at 5:57 PM, Johannes Weiß via swift-dev <swift-dev@swift.org> wrote:

Hi swift-dev,

I talked to a few people about this problem and we agreed that it is a problem and that it needs to be discussed. I didn't quite know where it would fit best but let's go with swift-dev, please feel free to tell to post it elsewhere if necessary. And apologies for the long mail, couldn't come up with a sensible tl;dr...

Let me briefly introduce the problem what for the lack of a better name I call 'signature package' or 'Service Provider Interface' (SPI) as some people from the Java community seem to be calling it (https://en.wikipedia.org/wiki/Service_provider_interface). For the rest of this email I'll use the term SPI.

In a large ecosystem there is a few pieces that many libraries will depend on and yet it seems pretty much impossible to standardise exactly one implementation. Logging is a very good example as many people have different ideas about how logging should and should not work. At the moment I guess your best bet is to use your preferred logging API and hope that all your other dependencies use the same one. If not you'll likely run into annoying problems (different sub-systems logging to different places or worse).

Also, in a world where some dependencies might be closed source this is an even bigger problem as clearly no open-source framework will depend on something that's not open-source.

In Java the way seems to be to standardise on some logging interface (read `protocol`) with different implementations. For logging that'd probably be SLF4J [4]. In Swift:

   let logger: LoggerProtocol = MyFavouriteLoggingFramework(configuration)

where `LoggerProtocol` comes from some SPI package and `MyFavouriteLoggingFramework` is basically what the name says. And as a general practise, everybody would only use `LoggerProtocol`. Then tomorrow when I'll change my mind replacing `MyFavouriteLoggingFramework` by `BetterFasterLoggingFramework` does the job. With 'dependency injection' this 'logger' is handed through the whole program and there's a good chance of it all working out. The benefits are that everybody just needs to agree on a `protocol` instead of an implementation. :+1:

In Swift the downside is that this means we're now getting a virtual dispatch and the existential everywhere (which in Java will be optimised away by the JIT). That might not be a huge problem but it might undermine 'CrazyFastLoggingFramework's adoption as we always pay overhead.

I don't think this problem can be elegantly solved today. What I could make work today (and maybe we could add language/SwiftPM support to facilitate it) is this (:warning:ÔłŹ, it's ugly)

- one SwiftPM package defines the SPI only, the only thing it exports is a `public protocol` called say `_spi_Logger`, no implementation
- every implementation of that SPI defines a `public struct Logger: _spi_Logger` (yes, they all share the _same_ name)
- every package that wants to log contains

   #if USE_FOO_LOGGER
       import FooLogger
   #elif USE_BAR_LOGGER
       import BarLogger
   #else
       import BuzLogger
   #endif

where 'BuzLogger' is the preferred logging system of this package but if either `USE_FOO_LOGGER` or `USE_BAR_LOGGER` was defined this package is happy to use those as well.
- `Logger` is always used as the type, it might be provided by different packages though
- in Package.swift of said package we'll need to define something like this:

    func loggingDependency() -> Package.Dependency {
    #if USE_FOO_LOGGER
        return .package(url: "github.com/...../foo.git", ...)
    #elif USE_BAR_LOGGER
        return ...
    #else
        return .package(url: "github.com/...../buz.git", ...)
    #endif
    }

     func loggingDependencyTarget() -> Target.Dependency {
    #if USE_FOO_LOGGER
        return "foo"
    #elif USE_BAR_LOGGER
        return "bar"
    #else
        return "buz"
    #endif
    }
- in the dependencies array of Package.swift we'll then use `loggingDependency()` and in the target we use `loggingDependencyTarget()` instead of the concrete one

Yes, it's awful but even in a world with different opinions about the implementation of a logger, we can make the program work.
In the happy case where application and all dependency agree that 'AwesomeLogging' is the best framework we can just type `swift build` and everything works. In the case where some dependencies think 'AwesomeLogging' is the best but others prefer 'BestEverLogging' we can force the whole application into one using `swift build -Xswiftc -DUSE_AWESOME_LOGGING` or `swift build -Xswiftc -DUSE_BEST_EVER_LOGGING`.

Wrapping up, I can see a few different options:

1) do nothing and live with the situation (no Swift/SwiftPM changes required)
2) advertise something similar to what I propose above (no Swift/SwiftPM changes required)
3) do what Java does but optimise the existential away at compile time (if the compiler can prove there's actually only one type that implements that protocol)
4) teach SwiftPM about those SPI packages and make everything work, maybe by textually replacing the import statements in the source?
5) do what Haskell did and retrofit a module system that can support this
6) have 'special' `specialized protocol` for which a concrete implementation needs to be selected by the primary source
7) something I haven't thought of

Btw, both Haskell (with the new 'backpack' [1, 2]) and ML have 'signatures' to solve this problem. A signature is basically an SPI. For an example see the backpack-str [3] module in Haskell which defines the signature (str-sig) and a bunch of different implementations for that signature (str-bytestring, str-string, str-foundation, str-text, ...).

Let me know what you think!

[1]: https://plv.mpi-sws.org/backpack/
[2]: https://ghc.haskell.org/trac/ghc/wiki/Backpack
[3]: https://github.com/haskell-backpack/backpack-str
[4]: https://www.slf4j.org

-- Johannes
PS: I attached a tar ball which contains the following 6 SwiftPM packages that are created like I describe above:

- app, the main application, prefers the 'foo' logging library
- somelibA, some library which logs and prefers the 'foo' logging library
- somelibB, some other library which prefers the 'bar' logging library
- foo, the 'foo' logging library
- bar, the 'bar' logging library
- spi, the logging SPI

The dependency default graph looks like this:
     +- somelibA ---+ foo
    / / \
app +--------------/ +-- spi
    \ /
     +- somelibB ---- bar

that looks all good, except that 'foo' and 'bar' are two logging libraries :see_no_evil:. In other words, we're in the unhappy case, therefore just typing `swift build` gives this:

--- SNIP ---
-1- johannes:~/devel/swift-spi-demo/app
$ swift build
Compile Swift Module 'app' (1 sources)
/Users/johannes/devel/swift-spi-demo/app/Sources/app/main.swift:14:23: error: cannot convert value of type 'Logger' to expected argument type 'Logger'
somelibB_func(logger: logger)
                     ^~~~~~
error: terminated(1): /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-build-tool -f /Users/johannes/devel/swift-spi-demo/app/.build/debug.yaml main
--- SNAP ---

because there's two `Logger` types. But selecting `foo` gives (note that all lines start with 'Foo:'):

--- SNIP ---
$ swift build -Xswiftc -DUSE_FOO
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Foo: info: hello from the app
Foo: info: hello from somelibA
Foo: info: hello from somelibB
Foo: info: hello from somelibA
Foo: info: hello from somelibB
--- SNAP ---

and for 'bar' (note that all lines start with 'Bar:')

--- SNIP ---
$ swift build -Xswiftc -DUSE_BAR
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Bar: info: hello from the app
Bar: info: hello from somelibA
Bar: info: hello from somelibB
Bar: info: hello from somelibA
Bar: info: hello from somelibB
--- SNAP ---

<swift-spi-demo.tar.gz>

_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev


(Johannes Weiss) #3

Hi Daniel,

My personal preference is to:
1. Do nothing for now, but encourage publishing standardized protocols to solve this need.
2. Hope for a future with WMO+LTO magic which recovers the performance, for the case where the entire application ends up using one implementation.

Hmm, but that'll only work if we get 'whole product optimisation', right? If we still compile one module at the time I don't think the compiler will be able to figure out that there's just one implementation of that protocol in the whole program. In fact it can't as that module might be linked into different programs and one of those programs might have a second implementation of that protocol. This is extremely likely as the 'test program' might have a mock/fake or some special implementation. Did I misunderstand you here?

You can manage some of the ‚Äúdependency injection‚ÄĚ part by making the package which exports the common protocol also export a global variable for the concrete implementation in use (with setters). That could just be a ‚Äúpattern‚ÄĚ people follow. This wouldn‚Äôt be particularly pretty, but it would mean that intermediate packages could avoid declaring a concrete dependency on any one implementation, and leave it up to clients to pick.

hmm, two questions:
- what would the type of that global variable be? All libraries in the program will need to know and agree on that type
- sounds like ThreadSanitizer would trap here unless we synchronise it which would make it slow again

-- Johannes

···

On 2 Nov 2017, at 8:15 pm, Daniel Dunbar <daniel_dunbar@apple.com> wrote:

- Daniel

On Nov 2, 2017, at 5:57 PM, Johannes Weiß via swift-dev <swift-dev@swift.org> wrote:

Hi swift-dev,

I talked to a few people about this problem and we agreed that it is a problem and that it needs to be discussed. I didn't quite know where it would fit best but let's go with swift-dev, please feel free to tell to post it elsewhere if necessary. And apologies for the long mail, couldn't come up with a sensible tl;dr...

Let me briefly introduce the problem what for the lack of a better name I call 'signature package' or 'Service Provider Interface' (SPI) as some people from the Java community seem to be calling it (https://en.wikipedia.org/wiki/Service_provider_interface). For the rest of this email I'll use the term SPI.

In a large ecosystem there is a few pieces that many libraries will depend on and yet it seems pretty much impossible to standardise exactly one implementation. Logging is a very good example as many people have different ideas about how logging should and should not work. At the moment I guess your best bet is to use your preferred logging API and hope that all your other dependencies use the same one. If not you'll likely run into annoying problems (different sub-systems logging to different places or worse).

Also, in a world where some dependencies might be closed source this is an even bigger problem as clearly no open-source framework will depend on something that's not open-source.

In Java the way seems to be to standardise on some logging interface (read `protocol`) with different implementations. For logging that'd probably be SLF4J [4]. In Swift:

  let logger: LoggerProtocol = MyFavouriteLoggingFramework(configuration)

where `LoggerProtocol` comes from some SPI package and `MyFavouriteLoggingFramework` is basically what the name says. And as a general practise, everybody would only use `LoggerProtocol`. Then tomorrow when I'll change my mind replacing `MyFavouriteLoggingFramework` by `BetterFasterLoggingFramework` does the job. With 'dependency injection' this 'logger' is handed through the whole program and there's a good chance of it all working out. The benefits are that everybody just needs to agree on a `protocol` instead of an implementation. :+1:

In Swift the downside is that this means we're now getting a virtual dispatch and the existential everywhere (which in Java will be optimised away by the JIT). That might not be a huge problem but it might undermine 'CrazyFastLoggingFramework's adoption as we always pay overhead.

I don't think this problem can be elegantly solved today. What I could make work today (and maybe we could add language/SwiftPM support to facilitate it) is this (:warning:ÔłŹ, it's ugly)

- one SwiftPM package defines the SPI only, the only thing it exports is a `public protocol` called say `_spi_Logger`, no implementation
- every implementation of that SPI defines a `public struct Logger: _spi_Logger` (yes, they all share the _same_ name)
- every package that wants to log contains

  #if USE_FOO_LOGGER
      import FooLogger
  #elif USE_BAR_LOGGER
      import BarLogger
  #else
      import BuzLogger
  #endif

where 'BuzLogger' is the preferred logging system of this package but if either `USE_FOO_LOGGER` or `USE_BAR_LOGGER` was defined this package is happy to use those as well.
- `Logger` is always used as the type, it might be provided by different packages though
- in Package.swift of said package we'll need to define something like this:

   func loggingDependency() -> Package.Dependency {
   #if USE_FOO_LOGGER
       return .package(url: "github.com/...../foo.git", ...)
   #elif USE_BAR_LOGGER
       return ...
   #else
       return .package(url: "github.com/...../buz.git", ...)
   #endif
   }

    func loggingDependencyTarget() -> Target.Dependency {
   #if USE_FOO_LOGGER
       return "foo"
   #elif USE_BAR_LOGGER
       return "bar"
   #else
       return "buz"
   #endif
   }
- in the dependencies array of Package.swift we'll then use `loggingDependency()` and in the target we use `loggingDependencyTarget()` instead of the concrete one

Yes, it's awful but even in a world with different opinions about the implementation of a logger, we can make the program work.
In the happy case where application and all dependency agree that 'AwesomeLogging' is the best framework we can just type `swift build` and everything works. In the case where some dependencies think 'AwesomeLogging' is the best but others prefer 'BestEverLogging' we can force the whole application into one using `swift build -Xswiftc -DUSE_AWESOME_LOGGING` or `swift build -Xswiftc -DUSE_BEST_EVER_LOGGING`.

Wrapping up, I can see a few different options:

1) do nothing and live with the situation (no Swift/SwiftPM changes required)
2) advertise something similar to what I propose above (no Swift/SwiftPM changes required)
3) do what Java does but optimise the existential away at compile time (if the compiler can prove there's actually only one type that implements that protocol)
4) teach SwiftPM about those SPI packages and make everything work, maybe by textually replacing the import statements in the source?
5) do what Haskell did and retrofit a module system that can support this
6) have 'special' `specialized protocol` for which a concrete implementation needs to be selected by the primary source
7) something I haven't thought of

Btw, both Haskell (with the new 'backpack' [1, 2]) and ML have 'signatures' to solve this problem. A signature is basically an SPI. For an example see the backpack-str [3] module in Haskell which defines the signature (str-sig) and a bunch of different implementations for that signature (str-bytestring, str-string, str-foundation, str-text, ...).

Let me know what you think!

[1]: https://plv.mpi-sws.org/backpack/
[2]: https://ghc.haskell.org/trac/ghc/wiki/Backpack
[3]: https://github.com/haskell-backpack/backpack-str
[4]: https://www.slf4j.org

-- Johannes
PS: I attached a tar ball which contains the following 6 SwiftPM packages that are created like I describe above:

- app, the main application, prefers the 'foo' logging library
- somelibA, some library which logs and prefers the 'foo' logging library
- somelibB, some other library which prefers the 'bar' logging library
- foo, the 'foo' logging library
- bar, the 'bar' logging library
- spi, the logging SPI

The dependency default graph looks like this:
    +- somelibA ---+ foo
   / / \
app +--------------/ +-- spi
   \ /
    +- somelibB ---- bar

that looks all good, except that 'foo' and 'bar' are two logging libraries :see_no_evil:. In other words, we're in the unhappy case, therefore just typing `swift build` gives this:

--- SNIP ---
-1- johannes:~/devel/swift-spi-demo/app
$ swift build
Compile Swift Module 'app' (1 sources)
/Users/johannes/devel/swift-spi-demo/app/Sources/app/main.swift:14:23: error: cannot convert value of type 'Logger' to expected argument type 'Logger'
somelibB_func(logger: logger)
                    ^~~~~~
error: terminated(1): /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-build-tool -f /Users/johannes/devel/swift-spi-demo/app/.build/debug.yaml main
--- SNAP ---

because there's two `Logger` types. But selecting `foo` gives (note that all lines start with 'Foo:'):

--- SNIP ---
$ swift build -Xswiftc -DUSE_FOO
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Foo: info: hello from the app
Foo: info: hello from somelibA
Foo: info: hello from somelibB
Foo: info: hello from somelibA
Foo: info: hello from somelibB
--- SNAP ---

and for 'bar' (note that all lines start with 'Bar:')

--- SNIP ---
$ swift build -Xswiftc -DUSE_BAR
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Bar: info: hello from the app
Bar: info: hello from somelibA
Bar: info: hello from somelibB
Bar: info: hello from somelibA
Bar: info: hello from somelibB
--- SNAP ---

<swift-spi-demo.tar.gz>

_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev


(Erik Eckstein) #4

Hi Daniel,

My personal preference is to:
1. Do nothing for now, but encourage publishing standardized protocols to solve this need.
2. Hope for a future with WMO+LTO magic which recovers the performance, for the case where the entire application ends up using one implementation.

Hmm, but that'll only work if we get 'whole product optimisation', right?

yes.

Even when we have cross-module optimizations (which would be comparable to thin-lto) we could not do that optimization.

If we still compile one module at the time I don't think the compiler will be able to figure out that there's just one implementation of that protocol in the whole program.

exactly

···

On Nov 8, 2017, at 5:27 PM, Johannes Weiß via swift-dev <swift-dev@swift.org> wrote:

On 2 Nov 2017, at 8:15 pm, Daniel Dunbar <daniel_dunbar@apple.com> wrote:

In fact it can't as that module might be linked into different programs and one of those programs might have a second implementation of that protocol. This is extremely likely as the 'test program' might have a mock/fake or some special implementation. Did I misunderstand you here?

You can manage some of the ‚Äúdependency injection‚ÄĚ part by making the package which exports the common protocol also export a global variable for the concrete implementation in use (with setters). That could just be a ‚Äúpattern‚ÄĚ people follow. This wouldn‚Äôt be particularly pretty, but it would mean that intermediate packages could avoid declaring a concrete dependency on any one implementation, and leave it up to clients to pick.

hmm, two questions:
- what would the type of that global variable be? All libraries in the program will need to know and agree on that type
- sounds like ThreadSanitizer would trap here unless we synchronise it which would make it slow again

-- Johannes

- Daniel

On Nov 2, 2017, at 5:57 PM, Johannes Weiß via swift-dev <swift-dev@swift.org> wrote:

Hi swift-dev,

I talked to a few people about this problem and we agreed that it is a problem and that it needs to be discussed. I didn't quite know where it would fit best but let's go with swift-dev, please feel free to tell to post it elsewhere if necessary. And apologies for the long mail, couldn't come up with a sensible tl;dr...

Let me briefly introduce the problem what for the lack of a better name I call 'signature package' or 'Service Provider Interface' (SPI) as some people from the Java community seem to be calling it (https://en.wikipedia.org/wiki/Service_provider_interface). For the rest of this email I'll use the term SPI.

In a large ecosystem there is a few pieces that many libraries will depend on and yet it seems pretty much impossible to standardise exactly one implementation. Logging is a very good example as many people have different ideas about how logging should and should not work. At the moment I guess your best bet is to use your preferred logging API and hope that all your other dependencies use the same one. If not you'll likely run into annoying problems (different sub-systems logging to different places or worse).

Also, in a world where some dependencies might be closed source this is an even bigger problem as clearly no open-source framework will depend on something that's not open-source.

In Java the way seems to be to standardise on some logging interface (read `protocol`) with different implementations. For logging that'd probably be SLF4J [4]. In Swift:

let logger: LoggerProtocol = MyFavouriteLoggingFramework(configuration)

where `LoggerProtocol` comes from some SPI package and `MyFavouriteLoggingFramework` is basically what the name says. And as a general practise, everybody would only use `LoggerProtocol`. Then tomorrow when I'll change my mind replacing `MyFavouriteLoggingFramework` by `BetterFasterLoggingFramework` does the job. With 'dependency injection' this 'logger' is handed through the whole program and there's a good chance of it all working out. The benefits are that everybody just needs to agree on a `protocol` instead of an implementation. :+1:

In Swift the downside is that this means we're now getting a virtual dispatch and the existential everywhere (which in Java will be optimised away by the JIT). That might not be a huge problem but it might undermine 'CrazyFastLoggingFramework's adoption as we always pay overhead.

I don't think this problem can be elegantly solved today. What I could make work today (and maybe we could add language/SwiftPM support to facilitate it) is this (:warning:ÔłŹ, it's ugly)

- one SwiftPM package defines the SPI only, the only thing it exports is a `public protocol` called say `_spi_Logger`, no implementation
- every implementation of that SPI defines a `public struct Logger: _spi_Logger` (yes, they all share the _same_ name)
- every package that wants to log contains

#if USE_FOO_LOGGER
     import FooLogger
#elif USE_BAR_LOGGER
     import BarLogger
#else
     import BuzLogger
#endif

where 'BuzLogger' is the preferred logging system of this package but if either `USE_FOO_LOGGER` or `USE_BAR_LOGGER` was defined this package is happy to use those as well.
- `Logger` is always used as the type, it might be provided by different packages though
- in Package.swift of said package we'll need to define something like this:

  func loggingDependency() -> Package.Dependency {
  #if USE_FOO_LOGGER
      return .package(url: "github.com/...../foo.git", ...)
  #elif USE_BAR_LOGGER
      return ...
  #else
      return .package(url: "github.com/...../buz.git", ...)
  #endif
  }

   func loggingDependencyTarget() -> Target.Dependency {
  #if USE_FOO_LOGGER
      return "foo"
  #elif USE_BAR_LOGGER
      return "bar"
  #else
      return "buz"
  #endif
  }
- in the dependencies array of Package.swift we'll then use `loggingDependency()` and in the target we use `loggingDependencyTarget()` instead of the concrete one

Yes, it's awful but even in a world with different opinions about the implementation of a logger, we can make the program work.
In the happy case where application and all dependency agree that 'AwesomeLogging' is the best framework we can just type `swift build` and everything works. In the case where some dependencies think 'AwesomeLogging' is the best but others prefer 'BestEverLogging' we can force the whole application into one using `swift build -Xswiftc -DUSE_AWESOME_LOGGING` or `swift build -Xswiftc -DUSE_BEST_EVER_LOGGING`.

Wrapping up, I can see a few different options:

1) do nothing and live with the situation (no Swift/SwiftPM changes required)
2) advertise something similar to what I propose above (no Swift/SwiftPM changes required)
3) do what Java does but optimise the existential away at compile time (if the compiler can prove there's actually only one type that implements that protocol)
4) teach SwiftPM about those SPI packages and make everything work, maybe by textually replacing the import statements in the source?
5) do what Haskell did and retrofit a module system that can support this
6) have 'special' `specialized protocol` for which a concrete implementation needs to be selected by the primary source
7) something I haven't thought of

Btw, both Haskell (with the new 'backpack' [1, 2]) and ML have 'signatures' to solve this problem. A signature is basically an SPI. For an example see the backpack-str [3] module in Haskell which defines the signature (str-sig) and a bunch of different implementations for that signature (str-bytestring, str-string, str-foundation, str-text, ...).

Let me know what you think!

[1]: https://plv.mpi-sws.org/backpack/
[2]: https://ghc.haskell.org/trac/ghc/wiki/Backpack
[3]: https://github.com/haskell-backpack/backpack-str
[4]: https://www.slf4j.org

-- Johannes
PS: I attached a tar ball which contains the following 6 SwiftPM packages that are created like I describe above:

- app, the main application, prefers the 'foo' logging library
- somelibA, some library which logs and prefers the 'foo' logging library
- somelibB, some other library which prefers the 'bar' logging library
- foo, the 'foo' logging library
- bar, the 'bar' logging library
- spi, the logging SPI

The dependency default graph looks like this:
   +- somelibA ---+ foo
  / / \
app +--------------/ +-- spi
  \ /
   +- somelibB ---- bar

that looks all good, except that 'foo' and 'bar' are two logging libraries :see_no_evil:. In other words, we're in the unhappy case, therefore just typing `swift build` gives this:

--- SNIP ---
-1- johannes:~/devel/swift-spi-demo/app
$ swift build
Compile Swift Module 'app' (1 sources)
/Users/johannes/devel/swift-spi-demo/app/Sources/app/main.swift:14:23: error: cannot convert value of type 'Logger' to expected argument type 'Logger'
somelibB_func(logger: logger)
                   ^~~~~~
error: terminated(1): /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-build-tool -f /Users/johannes/devel/swift-spi-demo/app/.build/debug.yaml main
--- SNAP ---

because there's two `Logger` types. But selecting `foo` gives (note that all lines start with 'Foo:'):

--- SNIP ---
$ swift build -Xswiftc -DUSE_FOO
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Foo: info: hello from the app
Foo: info: hello from somelibA
Foo: info: hello from somelibB
Foo: info: hello from somelibA
Foo: info: hello from somelibB
--- SNAP ---

and for 'bar' (note that all lines start with 'Bar:')

--- SNIP ---
$ swift build -Xswiftc -DUSE_BAR
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Bar: info: hello from the app
Bar: info: hello from somelibA
Bar: info: hello from somelibB
Bar: info: hello from somelibA
Bar: info: hello from somelibB
--- SNAP ---

<swift-spi-demo.tar.gz>

_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev

_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev


(Daniel Dunbar) #5

Hi Daniel,

My personal preference is to:
1. Do nothing for now, but encourage publishing standardized protocols to solve this need.
2. Hope for a future with WMO+LTO magic which recovers the performance, for the case where the entire application ends up using one implementation.

Hmm, but that'll only work if we get 'whole product optimisation', right? If we still compile one module at the time I don't think the compiler will be able to figure out that there's just one implementation of that protocol in the whole program. In fact it can't as that module might be linked into different programs and one of those programs might have a second implementation of that protocol. This is extremely likely as the 'test program' might have a mock/fake or some special implementation. Did I misunderstand you here?

That’s correct, that is what I meant by magic WMO+LTO future.

You can manage some of the ‚Äúdependency injection‚ÄĚ part by making the package which exports the common protocol also export a global variable for the concrete implementation in use (with setters). That could just be a ‚Äúpattern‚ÄĚ people follow. This wouldn‚Äôt be particularly pretty, but it would mean that intermediate packages could avoid declaring a concrete dependency on any one implementation, and leave it up to clients to pick.

hmm, two questions:
- what would the type of that global variable be? All libraries in the program will need to know and agree on that type

It would be the type of the abstract protocol.

- sounds like ThreadSanitizer would trap here unless we synchronise it which would make it slow again

Depends on how the protocol is phrased. The global instance could be a type which is instantiated and then doesn’t require locking.

An example of what I had in mind (Package.swifts left to the reader):

$ find . -name \*.swift -exec printf "**** %s ****\n" {} \; -exec cat {} \;
**** ./Client/Sources/Client/main.swift ****
import Log
import BadLogger

Log.registerLogger(BadLogger.self)
let logger = Log.createLogger()!
logger.log("how quaint")

**** ./BadLogger/Sources/BadLogger/BadLogger.swift ****
import Log

public struct BadLogger: Logger {
    public init() {}
    public func log(_ message: String) {
        fatalError("logging considered harmful: \(message)")
    }
}

**** ./Log/Sources/Log/Log.swift ****
import Dispatch

public protocol Logger {
    // MARK: Logger API
    
    init()
    func log(_ message: String)
}

// MARK: Global Registration

private let queue = DispatchQueue(label: "org.awesome.log")
private var theLoggerType: Logger.Type? = nil

public func registerLogger(_ type: Logger.Type) {
    queue.sync {
        theLoggerType = type
    }
}

public func createLogger() -> Logger? {
    return queue.sync{ theLoggerType }?.init()
}

I’m not saying this is the best solution in the world, but it does work currently without requiring new features. I agree there is a (small) performance cost, but for most use cases I doubt that is the most important consideration.

- Daniel

···

On Nov 8, 2017, at 5:27 PM, Johannes Weiß <johannesweiss@apple.com> wrote:

On 2 Nov 2017, at 8:15 pm, Daniel Dunbar <daniel_dunbar@apple.com> wrote:

-- Johannes

- Daniel

On Nov 2, 2017, at 5:57 PM, Johannes Weiß via swift-dev <swift-dev@swift.org> wrote:

Hi swift-dev,

I talked to a few people about this problem and we agreed that it is a problem and that it needs to be discussed. I didn't quite know where it would fit best but let's go with swift-dev, please feel free to tell to post it elsewhere if necessary. And apologies for the long mail, couldn't come up with a sensible tl;dr...

Let me briefly introduce the problem what for the lack of a better name I call 'signature package' or 'Service Provider Interface' (SPI) as some people from the Java community seem to be calling it (https://en.wikipedia.org/wiki/Service_provider_interface). For the rest of this email I'll use the term SPI.

In a large ecosystem there is a few pieces that many libraries will depend on and yet it seems pretty much impossible to standardise exactly one implementation. Logging is a very good example as many people have different ideas about how logging should and should not work. At the moment I guess your best bet is to use your preferred logging API and hope that all your other dependencies use the same one. If not you'll likely run into annoying problems (different sub-systems logging to different places or worse).

Also, in a world where some dependencies might be closed source this is an even bigger problem as clearly no open-source framework will depend on something that's not open-source.

In Java the way seems to be to standardise on some logging interface (read `protocol`) with different implementations. For logging that'd probably be SLF4J [4]. In Swift:

let logger: LoggerProtocol = MyFavouriteLoggingFramework(configuration)

where `LoggerProtocol` comes from some SPI package and `MyFavouriteLoggingFramework` is basically what the name says. And as a general practise, everybody would only use `LoggerProtocol`. Then tomorrow when I'll change my mind replacing `MyFavouriteLoggingFramework` by `BetterFasterLoggingFramework` does the job. With 'dependency injection' this 'logger' is handed through the whole program and there's a good chance of it all working out. The benefits are that everybody just needs to agree on a `protocol` instead of an implementation. :+1:

In Swift the downside is that this means we're now getting a virtual dispatch and the existential everywhere (which in Java will be optimised away by the JIT). That might not be a huge problem but it might undermine 'CrazyFastLoggingFramework's adoption as we always pay overhead.

I don't think this problem can be elegantly solved today. What I could make work today (and maybe we could add language/SwiftPM support to facilitate it) is this (:warning:ÔłŹ, it's ugly)

- one SwiftPM package defines the SPI only, the only thing it exports is a `public protocol` called say `_spi_Logger`, no implementation
- every implementation of that SPI defines a `public struct Logger: _spi_Logger` (yes, they all share the _same_ name)
- every package that wants to log contains

#if USE_FOO_LOGGER
    import FooLogger
#elif USE_BAR_LOGGER
    import BarLogger
#else
    import BuzLogger
#endif

where 'BuzLogger' is the preferred logging system of this package but if either `USE_FOO_LOGGER` or `USE_BAR_LOGGER` was defined this package is happy to use those as well.
- `Logger` is always used as the type, it might be provided by different packages though
- in Package.swift of said package we'll need to define something like this:

func loggingDependency() -> Package.Dependency {
#if USE_FOO_LOGGER
     return .package(url: "github.com/...../foo.git", ...)
#elif USE_BAR_LOGGER
     return ...
#else
     return .package(url: "github.com/...../buz.git", ...)
#endif
}

  func loggingDependencyTarget() -> Target.Dependency {
#if USE_FOO_LOGGER
     return "foo"
#elif USE_BAR_LOGGER
     return "bar"
#else
     return "buz"
#endif
}
- in the dependencies array of Package.swift we'll then use `loggingDependency()` and in the target we use `loggingDependencyTarget()` instead of the concrete one

Yes, it's awful but even in a world with different opinions about the implementation of a logger, we can make the program work.
In the happy case where application and all dependency agree that 'AwesomeLogging' is the best framework we can just type `swift build` and everything works. In the case where some dependencies think 'AwesomeLogging' is the best but others prefer 'BestEverLogging' we can force the whole application into one using `swift build -Xswiftc -DUSE_AWESOME_LOGGING` or `swift build -Xswiftc -DUSE_BEST_EVER_LOGGING`.

Wrapping up, I can see a few different options:

1) do nothing and live with the situation (no Swift/SwiftPM changes required)
2) advertise something similar to what I propose above (no Swift/SwiftPM changes required)
3) do what Java does but optimise the existential away at compile time (if the compiler can prove there's actually only one type that implements that protocol)
4) teach SwiftPM about those SPI packages and make everything work, maybe by textually replacing the import statements in the source?
5) do what Haskell did and retrofit a module system that can support this
6) have 'special' `specialized protocol` for which a concrete implementation needs to be selected by the primary source
7) something I haven't thought of

Btw, both Haskell (with the new 'backpack' [1, 2]) and ML have 'signatures' to solve this problem. A signature is basically an SPI. For an example see the backpack-str [3] module in Haskell which defines the signature (str-sig) and a bunch of different implementations for that signature (str-bytestring, str-string, str-foundation, str-text, ...).

Let me know what you think!

[1]: https://plv.mpi-sws.org/backpack/
[2]: https://ghc.haskell.org/trac/ghc/wiki/Backpack
[3]: https://github.com/haskell-backpack/backpack-str
[4]: https://www.slf4j.org

-- Johannes
PS: I attached a tar ball which contains the following 6 SwiftPM packages that are created like I describe above:

- app, the main application, prefers the 'foo' logging library
- somelibA, some library which logs and prefers the 'foo' logging library
- somelibB, some other library which prefers the 'bar' logging library
- foo, the 'foo' logging library
- bar, the 'bar' logging library
- spi, the logging SPI

The dependency default graph looks like this:
  +- somelibA ---+ foo
/ / \
app +--------------/ +-- spi
\ /
  +- somelibB ---- bar

that looks all good, except that 'foo' and 'bar' are two logging libraries :see_no_evil:. In other words, we're in the unhappy case, therefore just typing `swift build` gives this:

--- SNIP ---
-1- johannes:~/devel/swift-spi-demo/app
$ swift build
Compile Swift Module 'app' (1 sources)
/Users/johannes/devel/swift-spi-demo/app/Sources/app/main.swift:14:23: error: cannot convert value of type 'Logger' to expected argument type 'Logger'
somelibB_func(logger: logger)
                  ^~~~~~
error: terminated(1): /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-build-tool -f /Users/johannes/devel/swift-spi-demo/app/.build/debug.yaml main
--- SNAP ---

because there's two `Logger` types. But selecting `foo` gives (note that all lines start with 'Foo:'):

--- SNIP ---
$ swift build -Xswiftc -DUSE_FOO
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Foo: info: hello from the app
Foo: info: hello from somelibA
Foo: info: hello from somelibB
Foo: info: hello from somelibA
Foo: info: hello from somelibB
--- SNAP ---

and for 'bar' (note that all lines start with 'Bar:')

--- SNIP ---
$ swift build -Xswiftc -DUSE_BAR
Compile Swift Module 'spi' (1 sources)
Compile Swift Module 'foo' (1 sources)
Compile Swift Module 'bar' (1 sources)
Compile Swift Module 'somelibA' (1 sources)
Compile Swift Module 'somelibB' (1 sources)
Compile Swift Module 'app' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/debug/app
$ ./.build/x86_64-apple-macosx10.10/debug/app
Bar: info: hello from the app
Bar: info: hello from somelibA
Bar: info: hello from somelibB
Bar: info: hello from somelibA
Bar: info: hello from somelibB
--- SNAP ---

<swift-spi-demo.tar.gz>

_______________________________________________
swift-dev mailing list
swift-dev@swift.org
https://lists.swift.org/mailman/listinfo/swift-dev


(Joe Groff) #6

If you know you're building for an executable target, then it should be theoretically possible to look at the whole system and see that there's a single conformance to a protocol. For the situation Johannes is talking about, maybe this could be information that the build system feeds the compiler, so in a configuration file somewhere you'd say "I want to specialize all uses of the Logger.Logger protocol for the FancyLogger.FancyLogger<MyOutputStream> implementation" instead of relying on the compiler magically deriving it.

-Joe

···

On Nov 8, 2017, at 9:59 PM, Erik Eckstein via swift-dev <swift-dev@swift.org> wrote:

On Nov 8, 2017, at 5:27 PM, Johannes Weiß via swift-dev <swift-dev@swift.org> wrote:

Hi Daniel,

On 2 Nov 2017, at 8:15 pm, Daniel Dunbar <daniel_dunbar@apple.com> wrote:

My personal preference is to:
1. Do nothing for now, but encourage publishing standardized protocols to solve this need.
2. Hope for a future with WMO+LTO magic which recovers the performance, for the case where the entire application ends up using one implementation.

Hmm, but that'll only work if we get 'whole product optimisation', right?

yes.

Even when we have cross-module optimizations (which would be comparable to thin-lto) we could not do that optimization.

If we still compile one module at the time I don't think the compiler will be able to figure out that there's just one implementation of that protocol in the whole program.

exactly


(Erik Eckstein) #7

Hi Daniel,

My personal preference is to:
1. Do nothing for now, but encourage publishing standardized protocols to solve this need.
2. Hope for a future with WMO+LTO magic which recovers the performance, for the case where the entire application ends up using one implementation.

Hmm, but that'll only work if we get 'whole product optimisation', right?

yes.

Even when we have cross-module optimizations (which would be comparable to thin-lto) we could not do that optimization.

If we still compile one module at the time I don't think the compiler will be able to figure out that there's just one implementation of that protocol in the whole program.

exactly

If you know you're building for an executable target, then it should be theoretically possible to look at the whole system and see that there's a single conformance to a protocol. For the situation Johannes is talking about, maybe this could be information that the build system feeds the compiler, so in a configuration file somewhere you'd say "I want to specialize all uses of the Logger.Logger protocol for the FancyLogger.FancyLogger<MyOutputStream> implementation" instead of relying on the compiler magically deriving it.

Actually, speculative-devirtualization of protocol methods + cross-module optimization should be able to handle this. The only drawback is code size, which is fortunately not so important for that project.

···

On Nov 10, 2017, at 3:05 PM, Joe Groff <jgroff@apple.com> wrote:

On Nov 8, 2017, at 9:59 PM, Erik Eckstein via swift-dev <swift-dev@swift.org> wrote:

On Nov 8, 2017, at 5:27 PM, Johannes Weiß via swift-dev <swift-dev@swift.org> wrote:

On 2 Nov 2017, at 8:15 pm, Daniel Dunbar <daniel_dunbar@apple.com> wrote:

-Joe


(Johannes Weiss) #8

@Erik_Eckstein, I actually missed this reply almost a year ago. Just to be 100% sure, I'm right in thinking that there's no way to get speculative devirt work across modules today?

I'm asking logging discussion just started in the Server Work Group and actually designing a logging system in Swift is quite a bad place to be in:

  • people want to swap concrete logger implementations -> need an existential Logger
  • @autoclosure would be good for logging -> does allocations (unless we can get it inlined but we can't because it crosses modules so speculative devirt won't work (AFAIK))
  • to read the logger configuration (like minimum log level) you need a lock (because no memory model/atomics right now) unless you have one designated logger thread/process
  • you could firehose all logs from all threads into one designated process/thread but that'll either need a lock (to append to a shared logging queue) or a syscall (to write to a pipe/send UDP packet) because there's currently no performant way to build a MPSC (multiple producer single consumer) queue as we're lacking the atomics

Even reaching out to C to implement everything in a logging system isn't easy because to get from Swift's String a const char * we need to allocate too...

Maybe I should fork off a separate thread to discuss the performance issues that we'll see when implementing a logging library.


(Joe Groff) #9

Autoclosures are nonescaping, so this shouldn't be true anymore.

The "firehose" approach would have some benefit for your first point about needing an existential Logger, since if the logger interface is at the level of bulk-processing the firehose rather than of individual log operations, you can amortize the indirection cost. If you were to reach out to C to implement the firehose mechanism, it should be possible to teach the C code how to handle swift strings in their native form without conversion to char* (and we would also want to be able to process Swift string interpolations into constant C format strings for interop with facilities like os_log already).


(Johannes Weiss) #10

The repro in https://bugs.swift.org/browse/SR-7286 still works and still allocates from what I can tell with Xcode 10. Am I missing something?

I agree that this is possible but a third party package IMHO shouldn't reason about the internal layout of a Swift String, right?


(Joe Groff) #11

Ah, it's possible that we still haven't fully implemented stack allocation for nonescaping closures. Autoclosures don't fundamentally have to allocate, though.


(Johannes Weiss) #12
$ rm -rf .build/
johannes:/tmp/nectc
$ /Library/Developer/Toolchains/swift-4.2-RELEASE.xctoolchain/usr/bin/swift build -c release
Compile Swift Module 'OtherModule' (1 sources)
Compile Swift Module 'nectc' (1 sources)
Linking ./.build/x86_64-apple-macosx10.10/release/nectc
johannes:/tmp/nectc
$ lldb ./.build/x86_64-apple-macosx10.10/release/nectc
(lldb) target create "./.build/x86_64-apple-macosx10.10/release/nectc"
Current executable set to './.build/x86_64-apple-macosx10.10/release/nectc' (x86_64).
(lldb) break set -i 10000 -n malloc
Breakpoint 1: 14 locations.
(lldb) run
Process 8994 exited with status = 9 (0x00000009) 
Process 9003 launched: './.build/x86_64-apple-macosx10.10/release/nectc' (x86_64)
Process 9003 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00007fff7895c76b libsystem_malloc.dylib`malloc
libsystem_malloc.dylib`malloc:
->  0x7fff7895c76b <+0>: pushq  %rbp
    0x7fff7895c76c <+1>: movq   %rsp, %rbp
    0x7fff7895c76f <+4>: pushq  %rbx
    0x7fff7895c770 <+5>: pushq  %rax
Target 0: (nectc) stopped.
(lldb) bt
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
  * frame #0: 0x00007fff7895c76b libsystem_malloc.dylib`malloc
    frame #1: 0x00000001003f5289 libswiftCore.dylib`swift_slowAlloc + 25
    frame #2: 0x00000001003f52f4 libswiftCore.dylib`_swift_allocObject_(swift::TargetHeapMetadata<swift::InProcess> const*, unsigned long, unsigned long) + 20
    frame #3: 0x0000000100001307 nectc`nectc.foo() -> () + 71
    frame #4: 0x00000001000012b9 nectc`main + 9
    frame #5: 0x00007fff787af0a5 libdyld.dylib`start + 1
(lldb) 

and the according assembly:

    0x1000012f0 <+48>:  callq  0x100001270               ; OtherModule.Applicator.init() -> OtherModule.Applicator
    0x1000012f5 <+53>:  movl   $0x18, %esi
    0x1000012fa <+58>:  movl   $0x7, %edx
    0x1000012ff <+63>:  movq   %r14, %rdi
    0x100001302 <+66>:  callq  0x100003628               ; symbol stub for: swift_allocObject
    0x100001307 <+71>:  movq   %rax, %rbx
    0x10000130a <+74>:  movq   %r13, 0x10(%rbx)
    0x10000130e <+78>:  movq   %r15, %rdi
    0x100001311 <+81>:  movq   %rbx, %rsi
    0x100001314 <+84>:  callq  0x100001280               ; OtherModule.Applicator.apply(() -> ()) -> ()
    0x100001319 <+89>:  movq   %rbx, %rdi
    0x10000131c <+92>:  callq  0x10000363a               ; symbol stub for: swift_release
    0x100001321 <+97>:  decq   %r12

(Johannes Weiss) #13

I'm sure there are cases when then don't allocate but it's not too hard to find one that does. Changed the repro from SR-7286 to use @autoclosure. Here's the relevant assembly:

    0x1000012e0 <+48>:  callq  0x100001260               ; OtherModule.Applicator.init() -> OtherModule.Applicator
    0x1000012e5 <+53>:  movl   $0x18, %esi
    0x1000012ea <+58>:  movl   $0x7, %edx
    0x1000012ef <+63>:  movq   %r14, %rdi
    0x1000012f2 <+66>:  callq  0x10000361e               ; symbol stub for: swift_allocObject
    0x1000012f7 <+71>:  movq   %rax, %rbx
    0x1000012fa <+74>:  movq   %r13, 0x10(%rbx)
    0x1000012fe <+78>:  movq   %r15, %rdi
    0x100001301 <+81>:  movq   %rbx, %rsi
    0x100001304 <+84>:  callq  0x100001270               ; OtherModule.Applicator.apply(@autoclosure () -> Swift.Int) -> ()
    0x100001309 <+89>:  movq   %rbx, %rdi
    0x10000130c <+92>:  callq  0x100003630               ; symbol stub for: swift_release

the diff I applied to the nectc.tar.gz which is attached to the JIRA is

diff -ru original/nectc/Sources/OtherModule/OtherModule.swift nectc/Sources/OtherModule/OtherModule.swift
--- original/nectc/Sources/OtherModule/OtherModule.swift	2018-03-27 16:42:37.000000000 +0100
+++ nectc/Sources/OtherModule/OtherModule.swift	2018-09-20 16:57:04.000000000 +0100
@@ -1,6 +1,8 @@
 public struct Applicator {
     public init() {}
-    public func apply(_ body: () -> Void) -> Void {
-        body()
+    public func apply(_ body: @autoclosure () -> Int) -> Void {
+        let x = body()
+        let y = x
+        precondition(x == 1 || x == y)
     }
 }
diff -ru original/nectc/Sources/nectc/main.swift nectc/Sources/nectc/main.swift
--- original/nectc/Sources/nectc/main.swift	2018-03-27 16:48:31.000000000 +0100
+++ nectc/Sources/nectc/main.swift	2018-09-20 16:57:15.000000000 +0100
@@ -3,9 +3,8 @@
 func foo() {
     var counter = 0
     for _ in 0..<100_000 {
-        Applicator().apply {
-            counter += 1
-        }
+        Applicator().apply(counter)
+        counter += 1
     }
     print(counter)
 }

(Johannes Weiss) #14

Maybe 'just' @autoclosures that cross modules are heap allocated? But for a logging system that can't be fully inlined (because existentials) that would still mean an allocation every time, no?


(Joe Groff) #15

There is never any reason a nonescaping closure has to allocate. These are all bugs.


(Johannes Weiss) #16

I totally agree (hence filing the bug) but it hasn't been fixed so Swift as of today does allocate for @autoclosure arguments in many cases, seemingly most often across modules.

The same thing applies to SR-4937 which I filed over a year ago.

So far I only ever had luck getting any sort of closure not to allocate was to make them @inlinable (or stick caller and callee into the same module which has the same effect).


(Joe Groff) #17

Sure. If an API design relies on efficient autoclosures, we can see whether we can finish the work to make them non-heap-allocating.


(Johannes Weiss) #18

Thanks! Will add that to the logging discussion.