Control symbol visibility

Controlling symbol visibility can reduce APK size, improve load times, and help other developers avoid accidental dependencies on implementation details. The most robust way to do this is with version scripts.

Version scripts are a feature of ELF linkers that can be used as a more robust form of -fvisibility=hidden. See Benefits below for a more detailed explanation, or read on to learn how to use version scripts in your project.

In the GNU documentation linked above and in a few other places on this page, you'll see references to "symbol versions". That's because the original intent for these files was to allow multiple versions of a symbol (usually a function) to exist in a single library for bug-compatibility preservation in libraries. Android supports that use as well, but it's generally only of use to OS library vendors, and even we don't use them in Android because targetSdkVersion offers the same benefits with a more deliberate opt-in process. For the topic of this doc, don't worry about terms like "symbol version". If you're not defining multiple versions of the same symbol, "symbol version" is just an arbitrary named grouping of symbols in the file.

If you're a library author (whether your interface is C/C++, or if it's Java/Kotlin and your native code is merely an implementation detail) rather than an app developer, be sure to also read Advice for middleware vendors.

Write a version script

In the ideal case, an app (or AAR) that includes native code will contain exactly one shared library, with all its dependencies statically linked into that one library, and the complete public interface of that library is JNI_OnLoad. This allows the benefits described on this page to be applied as broadly as possible. In that case, assuming that library is named libapp.so, create a libapp.map.txt file (the name doesn't need to match, and the .map.txt suffix is just a convention) with the following contents (you can omit the comments):

# The name used here also doesn't matter. This is the name of the "version"
# which matters when the version script is actually used to create multiple
# versions of the same symbol, but that's not what we're doing.
LIBAPP {
  global:
    # Every symbol named in this section will have "default" (that is, public)
    # visibility. See below for how to refer to C++ symbols without mangling.
    JNI_OnLoad;
  local:
    # Every symbol in this section will have "local" (that is, hidden)
    # visibility. The wildcard * is used to indicate that all symbols not listed
    # in the global section should be hidden.
    *;
};

If your app has more than one shared library, you must add one version script per library.

For JNI libraries that are not using JNI_OnLoad and RegisterNatives(), you can instead list each of the JNI methods with their JNI mangled names.

For non-JNI libraries (dependencies of JNI libraries, typically), you'll need to enumerate your full API surface. If your interface is C++ rather than C, you can use extern "C++" { ... } in a version script the same way you would in a header file. For example:

LIBAPP {
  global:
    extern "C++" {
      # A class that exposes only some methods. Note that any methods that are
      # `private` in the class will still need to be visible in the library if
      # they are called by `inline` or `template` functions.
      #
      # Non-static members do not need to be enumerated as they do not have
      # symbols associated with them, but static members must be included.
      #
      # The * exposes all overloads of the MyClass constructor, but note that it
      # will also expose methods like MyClass::MyClassNonConstructor.
      MyClass::MyClass*;
      MyClass::DoSomething;
      MyClass::static_member;

      # All members/methods of a class, including those that are `private` in
      # the class.
      MyOtherClass::*;
      #

      # If you wish to only expose some overloads, name the full signature.
      # You'll need to wrap the name in quotes, otherwise you'll get a warning
      # like like "ignoring invalid character '(' in script" and the symbol will
      # remain hidden (pass -Wl,--no-undefined-version to convert that warning
      # to an error as described below).
      "MyClass::MyClass()";
      "MyClass::MyClass(const MyClass&)";
      "MyClass::~MyClass()";
    };
  local:
    *;
};

Use the version script when building

The version script must be passed to the linker when building. Follow the steps appropriate to your build system below.

CMake

# Assuming that your app library's target is named "app":
target_link_options(app
    PRIVATE
    -Wl,--version-script,${CMAKE_SOURCE_DIR}/libapp.map.txt
    # This causes the linker to emit an error when a version script names a
    # symbol that is not found, rather than silently ignoring that line.
    -Wl,--no-undefined-version
)

# Without this, changes to the version script will not cause the library to
# relink.
set_target_properties(app
    PROPERTIES
    LINK_DEPENDS ${CMAKE_SOURCE_DIR}/libapp.map.txt
)

ndk-build

# Add to an existing `BUILD_SHARED_LIBRARY` stanza (use `+=` instead of `:=` if
# the module already sets `LOCAL_LDFLAGS`):
LOCAL_LDFLAGS := -Wl,--version-script,$(LOCAL_PATH)/libapp.map.txt

# This causes the linker to emit an error when a version script names a symbol
# that is not found, rather than silently ignoring that line.
LOCAL_ALLOW_UNDEFINED_VERSION_SCRIPT_SYMBOLS := false

# ndk-build doesn't have a mechanism for specifying that libapp.map.txt is a
# dependency of the module. You may need to do a clean build or otherwise force
# the library to rebuild (such as by changing a source file) when altering the
# version script.

Other

If the build system you're using has explicit support for version scripts, use that.

Otherwise, use the following linker flags:

-Wl,--version-script,path/to/libapp.map.txt -Wl,--no-version-undefined

How those are specified will depend on your build system, but there's typically an option named LDFLAGS or something similar. path/to/libapp.map.txt needs to be resolvable from the current working directory of the linker. It's often easier to use an absolute path.

If you're not using a build system, or are a build system maintainer looking to add version script support, those flags should be passed to clang (or clang++) when linking but not when compiling.

Benefits

APK size can be improved when using a version script because it minimizes the visible set of symbols in a library. By telling the linker exactly which functions are accessible to callers, the linker can remove all the unreachable code from the library. This process is a type of dead-code elimination. The linker cannot remove the definition for function (or other symbol) that is not hidden, even if the function is never called, because the linker must assume that a visible symbol is a part of the library's public interface. Hiding symbols allows the linker to remove functions that are not called, reducing the size of the library.

Library load performance is improved for similar reasons: relocations are required for visible symbols because those symbols are interposable. That's almost never the desired behavior, but it what's required by the ELF specification, so it's the default. but because the linker can't know which (if any) symbols you intend to be interposable, it must create relocations for every visible symbol. Hiding those symbols allows the linker to omit those relocations in favor of direct jumps, which reduces the amount of work the dynamic linker must do when loading libraries.

Explicitly enumerating your API surface also prevents consumers of your libraries from mistakenly depending on implementation details of your library, as those details won't be visible.

Comparison with alternatives

Version scripts offer similar results as alternatives such as -fvisibility=hidden or per-function __attribute__((visibility("hidden"))). All three approaches control which symbols of a library are visible to other libraries and to dlsym.

The biggest downside to the other two approaches is that they are only able to hide symbols defined in the library being built. They cannot hide symbols from static library dependencies of the library. A very common case where this makes a difference is when using libc++_static.a. Even if your build uses -fvisibility=hidden, while the library's own symbols will be hidden, all the symbols included from libc++_static.a will become public symbols of your library. In contrast, version scripts offer explicit control of the public interface of the library; if the symbol is not explicitly listed as visible in the version script, it will be hidden.

The other difference can be both a pro and a con: the public interface of the library must be explicitly defined in a version script. For JNI libraries this is actually trivial, because the only necessary interface for a JNI library is JNI_OnLoad (because JNI methods registered with RegisterNatives() need not be public). For libraries with a large public interface this can be an additional maintenance burden, but one that's usually worthwhile.