skip to content

Why does the Kotlin compiler forbid a `public inline` function from referencing `private` members of its class, and how does `@PublishedApi` relate to this?

level: seniorimportance: should knowfreq 45%

answer

  1. Inline body runs with caller's visibility
  2. public inline can't touch private members
  3. Referenced members must be >= the function's visibility
  4. @PublishedApi exposes internal to inline code
  5. @PublishedApi only on internal; affects binary compat

basics

~20 s

Because an inline function's body gets copied into the code that calls it. If that body touched something private, the caller's compiled code would reference a member it isn't allowed to see. @PublishedApi lets you mark an internal member as safe to expose to such inlined code.

solid answer

~50 s

An `inline` function's body is emitted at the *call site*, so it runs with the caller's visibility, not the declaring class's. If a `public` (or `protected`) inline function referenced a `private` member, callers in other classes/modules would end up invoking something they can't legally access — so the compiler rejects it: "Public-API inline function cannot access non-public-API members." Allowed references must themselves be at least as visible as the inline function. The escape hatch is `@PublishedApi`: you annotate an `internal` declaration with it, making it accessible to public inline functions (and thus to the inlined code in other modules) while keeping it out of the normal public API/IDE completion. This is exactly why inlining leaks implementation into consumers and constrains library evolution — you must consciously decide which internals become part of your effective binary surface via `@PublishedApi`.

code

kotlin · 11 lines
kotlin
internal class Engine                       // not visible outside module

// ERROR: public inline body can't reference an internal type
// inline fun run(block: (Engine) -> Unit) { block(Engine()) }

@PublishedApi
internal class PublishedEngine               // opt the internal into inline use

inline fun run(block: (PublishedEngine) -> Unit) {
    block(PublishedEngine())                 // now legal in a public inline fun
}

go deeper

for a junior

Recognizes that public inline can't use private members, even if shaky on why.

for a middle

Explains the call-site-visibility reason and knows @PublishedApi is the workaround for internal members.

for a senior

Articulates the >=-visibility rule precisely and that @PublishedApi only applies to internal, plus the basic binary-compat implication.

for a principal

Treats @PublishedApi as a deliberate widening of the binary contract and reasons about library evolution and ABI stability around inline.

## The root cause: body runs at the call site When you call a `public inline fun`, the compiler copies its body into the caller. That copied code executes with the **caller's** access rights. So any member the inline body touches must be visible from wherever the function can be called. Visibility levels, from least to most accessible: `private` < `internal`/`protected` < `public`. A `public inline` function can be called from any module, so its body may only reference members that are also reachable from any module — i.e. effectively `public`. ## The rule A `public` or `protected` inline function **cannot reference declarations less visible than itself** (e.g. `private`, plain `internal`). The compiler errors with something like *"Public-API inline function cannot access non-public-API ..."*. The same applies to `internal inline` referencing `private`. ```kotlin class Cache { private val store = HashMap<String, String>() // ERROR: public inline body would copy a reference to private `store` // into callers that cannot see `store`. inline fun getOrPut(key: String, compute: () -> String): String = store[key] ?: compute().also { store[key] = it } } ``` ## The fix: `@PublishedApi` `@PublishedApi` marks an `internal` declaration as *part of the published API for inlining purposes*. It becomes accessible to public inline functions (and therefore to the inlined copies that land in other modules) but is hidden from normal IDE completion / treated as not-quite-public for everyday use. ```kotlin class Cache { @PublishedApi internal val store = HashMap<String, String>() // exposed only for inline use inline fun getOrPut(key: String, compute: () -> String): String = store[key] ?: compute().also { store[key] = it } // now OK } ``` Note you can only put `@PublishedApi` on `internal` members; it would be redundant on public and illegal on private. ## Why this is a tradeoff, not just a rule Because the inline body is baked into every consumer's compiled output, anything it references (including `@PublishedApi` internals) becomes part of your **effective binary contract**: - Renaming or changing the signature of a `@PublishedApi` member can break already-compiled callers (binary incompatibility) until they recompile. - It widens your maintenance surface: 'internal' is no longer truly private. So the visibility rule + `@PublishedApi` are the language making the cost of inlining explicit: you must opt internals into the public binary surface deliberately, rather than leaking them by accident.

  • Can you annotate a private member with @PublishedApi?
    No. @PublishedApi applies only to internal declarations. Private isn't supported, and on public members it's redundant/an error.
  • What's the binary-compatibility risk of @PublishedApi members?
    Their references are inlined into consumers' bytecode, so renaming or changing their signature can break already-compiled callers until they recompile — they're effectively part of your binary API.
  • Does an internal inline function face the same restriction?
    Yes, relative to its own visibility: an internal inline function still cannot reference private members, because those references would be copied where private isn't accessible.

saying these in an interview costs you the question

  • Saying the rule is arbitrary instead of a visibility/access consequence
  • Thinking @PublishedApi can be applied to private members
  • Not realizing inlined code carries the caller's access rights
  • Ignoring that @PublishedApi widens the binary contract
  • Confusing @PublishedApi with @JvmStatic or @Suppress

context