What does @inlinable change in a module’s .swiftinterface and in a client binary, and when is it worth the commitment?
Predict first
What does the interface show for each function, and what does the client still call?
// === Lib.swift ===
public struct Maths {
public init() {}
@inlinable public func double(_ x: Int) -> Int { x &* 2 }
public func triple(_ x: Int) -> Int { x &* 3 }
}
// === main.swift ===
import Lib
let m = Maths()
print(m.double(CommandLine.arguments.count), m.triple(CommandLine.arguments.count))
// === run.sh ===
swiftc -emit-module -emit-module-interface -enable-library-evolution -swift-version 6 -module-name Lib -emit-library -O Lib.swift -o libLib.dylib
echo "interface: $(grep -E 'func (double|triple)' Lib.swiftinterface | sed 's/^ *//' | tr '\n' '|')"
swiftc -O -swift-version 6 main.swift -I . -L . -lLib -o client
echo "runs: $(./client)"
echo "calls into the library for triple: $(nm -u client | swift demangle --simplified | grep -c 'Maths.triple')"
echo "calls into the library for double: $(nm -u client | swift demangle --simplified | grep -c 'Maths.double')"
- interface shows double’s body and triple’s signature; the client calls Maths.triple but not Maths.double
- both bodies appear in the interface
- the client calls both functions in the library
Mental model. @inlinable hands the customer the recipe, not just the dish: they cook it faster at home, but if you improve the recipe, their copy stays as it was.
Q45 explains the rule; here is the observable effect. A library built with library evolution (the setup for binary frameworks, Q366) emits a .swiftinterface, the textual public API that clients compile against. In the check, Maths.double is @inlinable and Maths.triple is not, and the interface shows the body of double ({ x &* 2 }) but only the signature of triple. The client, compiled with -O, then has no undefined symbol for Maths.double (the body was copied into the client and optimised there; the symbol count is 0) but does call Maths.triple in the library (count 1). So @inlinable trades speed for freedom: generic and small hot functions can be specialised in the client, but the body is now part of your ABI and API contract. Clients keep running the old body until they are recompiled, even after you ship a fixed library, and the body may only use public or @usableFromInline declarations.
When to use it: small, stable, performance-sensitive generic code in a binary or resilient library (collections, numeric helpers, property wrappers), where crossing the module boundary would otherwise block specialisation. When not to: anything that may need a bug fix without every client rebuilding, or that touches internals. Inside one Swift package built from source, whole-module optimisation and cross-module optimisation already allow inlining across modules without the commitment (the cross-module-optimization setting; I did not run that comparison, so measure before relying on it). The review habit: every @inlinable in a public library should have a reason in a comment and a test that runs against an older compiled client, which is what binary-compatibility checks in CI do.
An inlinable body is emitted in the interface and compiled into clients.
Common traps
- Marking large or internal-touching functions
@inlinable“for speed”. You freeze the body into every client. - Expecting a library bug fix to reach clients without a rebuild. Inlined bodies are already inside their binary.
They might follow up with
- How can I see the generated interface? Build with library evolution and open the
.swiftinterface(the check does it withswiftc -emit-module-interface). - What does
@usableFromInlineadd? It lets an inlinable body call an internal declaration without making it public API.
Related questions
- 16.9.2 What do public, package and internal imports change, and why can a client use members of a module it never…
- 1.7.4 What are @usableFromInline and @inlinable in relation to access control?
- 16.1.2 How do internal, package and public work between modules, and what are the classic surprises?
- 16.2.2 What is an XCFramework, what do module maps and library evolution have to do with it, and when do you use a…
- 16.3.1 How do static and dynamic linking differ for an app, what do they cost at launch and in size, and what are…
What do public, package and internal imports change, and why can a… →