Hỗ trợ thư viện C++

NDK hỗ trợ nhiều thư viện thời gian chạy C++. Tài liệu này cung cấp thông tin về các thư viện đó, các yếu tố đánh đổi có liên quan cũng như cách sử dụng các thư viện đó.

Thư viện thời gian chạy C++

Bảng 1. Các tính năng và Môi trường thời gian chạy C++ trong NDK.

Tên Tính năng
libc++ Hỗ trợ C++ hiện đại.
system newdelete. (Không dùng nữa trong r18.)
none Không có tiêu đề, C++ có giới hạn.

libc++ có sẵn cả dưới dạng thư viện tĩnh và thư viện dùng chung.

libc++

libc++ của LLVM là thư viện tiêu chuẩn C++ mà hệ điều hành Android sử dụng từ phiên bản Lollipop và là thư viện STL duy nhất có sẵn trong NDK kể từ phiên bản NDK r18.

CMake là mặc định đối với mọi phiên bản C++ có clang là mặc định (hiện là C++14), vì vậy bạn cần thiết lập CMAKE_CXX_STANDARD tiêu chuẩn thành giá trị thích hợp trong tệp CMakeLists.txt để sử dụng các tính năng C++17 trở lên. Xem tài liệu dành cho CMAKE_CXX_STANDARD của CMake để biết thêm chi tiết.

ndk-build cũng để cho clang quyết định theo mặc định, do đó người dùng ndk-build nên sử dụng APP_CPPFLAGS để thêm -std=c++17 hay nội dung bất kỳ mà họ muốn.

Thư viện dùng chung dành cho libc++ là libc++_shared.so, còn thư viện tĩnh là libc++_static.a. Trong các trường hợp thông thường, hệ thống xây dựng sẽ xử lý việc sử dụng và đóng gói các thư viện này nếu cần cho người dùng. Đối với các trường hợp không điển hình hoặc khi triển khai hệ thống xây dựng của riêng bạn, hãy xem Hướng dẫn bảo trì hệ thống xây dựng hoặc hướng dẫn sử dụng các hệ thống xây dựng khác.

Dự án LLVM thuộc phạm vi điều chỉnh của Giấy phép Apache phiên bản 2.0 với ngoại lệ LLVM. Để biết thêm thông tin, hãy xem nội dung tệp giấy phép.

system

Môi trường thời gian chạy của hệ thống tham chiếu đến /system/lib/libstdc++.so. Bạn không nên nhầm lẫn thư viện này với thư viện libstdc++ đầy đủ tính năng của GNU. Trên Android, libstdc++ chỉ là newdelete. Hãy sử dụng libc++ để có thư viện C++ tiêu chuẩn với đầy đủ tính năng.

Môi trường thời gian chạy C++ của hệ thống hỗ trợ ABI Thời gian chạy C++ cơ bản. Về cơ bản, thư viện này cung cấp newdelete. Khác với các tuỳ chọn khác có trong NDK, thư viện này không hỗ trợ việc xử lý ngoại lệ hoặc RTTI.

Không có tính năng hỗ trợ thư viện chuẩn nào ngoài các trình bao bọc C++ dành cho các tiêu đề thư viện C, chẳng hạn như <cstdio>. Nếu muốn sử dụng STL, bạn nên dùng một cách khác được trình bày trên trang này.

none

Cũng có cách để không có STL. Không có yêu cầu bắt buộc nào về việc liên kết hoặc cấp phép trong trường hợp đó. Hiện không có tiêu đề tiêu chuẩn C++.

Chọn một môi trường thời gian chạy C++

CMake

Giá trị mặc định cho CMake là c++_static.

Bạn có thể chỉ định c++_shared, c++_static, none hoặc system bằng cách sử dụng biến ANDROID_STL trong tệp build.gradle cấp mô-đun. Để tìm hiểu thêm, hãy xem tài liệu về ANDROID_STL trong CMake.

ndk-build

Giá trị mặc định cho ndk-build là none.

Bạn có thể chỉ định c++_shared, c++_static, none hoặc system bằng cách sử dụng biến APP_STL trong tệp Application.mk. Ví dụ:

APP_STL := c++_shared

ndk-build chỉ cho phép bạn chọn một môi trường thời gian chạy cho ứng dụng và cũng chỉ có thể thực hiện trong Application.mk.

Trực tiếp sử dụng clang

Nếu bạn đang trực tiếp sử dụng clang trong hệ thống xây dựng của riêng mình, clang++ sẽ sử dụng c++_shared theo mặc định. Để sử dụng biến thể tĩnh của thư viện, hãy thêm -static-libstdc++ vào các cờ liên kết của bạn. Xin lưu ý rằng mặc dù tuỳ chọn này sử dụng tên "libstdc++" vì lý do liên quan đến trước đây, nhưng điều này cũng đúng với libc++.

Điểm quan trọng cần lưu ý

Môi trường thời gian chạy tĩnh

Nếu tất cả mã gốc của ứng dụng đều được chứa trong một thư viện dùng chung, bạn nên sử dụng môi trường thời gian chạy tĩnh. Nhờ vậy, trình liên kết có thể nối tiếp và cắt giảm nhiều mã không sử dụng nhất có thể, giúp ứng dụng được tối ưu hoá và giảm kích thước tối đa có thể. Việc này cũng giúp tránh các lỗi PackageManager và lỗi trình liên kết động trong các phiên bản Android cũ. Các lỗi này khiến việc xử lý nhiều thư viện dùng chung gặp khó khăn và dễ gặp lỗi.

Tuy nhiên, trong C++, việc khai báo nhiều bản sao của cùng một hàm hoặc đối tượng trong cùng một chương trình là một việc không an toàn. Đây là một khía cạnh của Quy tắc một định nghĩa hiện diện trong tiêu chuẩn C++.

Khi sử dụng môi trường thời gian chạy tĩnh (và thư viện tĩnh nói chung), bạn sẽ dễ vô tình phá vỡ quy tắc này. Ví dụ ứng dụng sau đây vi phạm quy tắc này:

# Application.mk
APP_STL := c++_static
# Android.mk

include $(CLEAR_VARS)
LOCAL_MODULE := foo
LOCAL_SRC_FILES := foo.cpp
include $(BUILD_SHARED_LIBRARY)

include $(CLEAR_VARS)
LOCAL_MODULE := bar
LOCAL_SRC_FILES := bar.cpp
LOCAL_SHARED_LIBRARIES := foo
include $(BUILD_SHARED_LIBRARY)

Trong trường hợp này, STL (bao gồm cả dữ liệu toàn cục và hàm khởi tạo tĩnh) sẽ có trong cả hai thư viện. Hành vi thời gian chạy của ứng dụng này là không xác định và trong thực tế các lỗi trục trặc là rất phổ biến. Có thể xảy ra một số vấn đề khác như sau:

  • Bộ nhớ được phân bổ trong một thư viện và được giải phóng trong thư viện còn lại, gây ra hiện tượng rò rỉ bộ nhớ hoặc lỗi vùng nhớ khối xếp.
  • Các ngoại lệ được ghi nhận trong libfoo.so vẫn chưa được xử lý trong libbar.so, khiến ứng dụng của bạn bị lỗi.
  • Bộ đệm std::cout không hoạt động bình thường.

Ngoài các vấn đề liên quan đến hành vi, việc liên kết môi trường thời gian chạy tĩnh với nhiều thư viện sẽ làm nhân bản mã trong mỗi thư viện dùng chung, làm tăng kích thước của ứng dụng.

Nhìn chung, bạn chỉ có thể sử dụng một biến thể tĩnh của thời gian chạy C++ nếu có một và chỉ một thư viện dùng chung trong ứng dụng của bạn.

Môi trường thời gian chạy dùng chung

Nếu ứng dụng của bạn có nhiều thư viện dùng chung, bạn nên sử dụng libc++_shared.so.

Trên Android, libc++ mà NDK sử dụng không giống với libc++ là một phần của hệ điều hành. Việc này cho phép người dùng NDK truy cập vào các tính năng và bản sửa lỗi libc++ mới nhất ngay cả khi nhắm đến các phiên bản Android cũ. Sự đánh đổi ở đây là nếu sử dụng libc++_shared.so, bạn phải đưa thư viện này vào ứng dụng của mình. Nếu bạn đang xây dựng ứng dụng bằng Gradle, thì thao tác này sẽ được xử lý tự động.

Các phiên bản Android cũ vướng phải nhiều lỗi trong PackageManager và trình liên kết động, khiến quá trình cài đặt, cập nhật và tải thư viện gốc không đáng tin cậy. Cụ thể là nếu ứng dụng của bạn nhắm đến một phiên bản Android trước Android 4.3 (Android API cấp 18) và bạn sử dụng libc++_shared.so, thì bạn phải tải thư viện dùng chung đó trước mọi thư viện khác phụ thuộc vào thư viện dùng chung đó.

Dự án ReLinker đưa ra giải pháp cho mọi vấn đề đã biết về việc tải thư viện gốc. Thường thì đây cũng là lựa chọn tốt hơn so với việc tự viết giải pháp của riêng bạn.

Một STL cho mỗi ứng dụng

Trước đây, ngoài libc++ thì NDK còn hỗ trợ GNU libstdc++ và STLport. Nếu ứng dụng của bạn phụ thuộc vào các thư viện tạo sẵn được xây dựng dựa trên một phiên bản NDK khác với phiên bản dùng để xây dựng ứng dụng, bạn sẽ cần phải đảm bảo rằng việc này được thực hiện theo cách tương thích.

Một ứng dụng không được dùng nhiều môi trường thời gian chạy C++. Các STL không tương thích với nhau. Ví dụ: bố cục của std::string trong libc++ không giống như bố cục trong gnustl. Mã được viết dựa trên một STL sẽ không thể sử dụng các đối tượng được viết dựa trên các STL khác. Đây chỉ là một ví dụ; có rất nhiều trường hợp không tương thích.

Phạm vi áp dụng quy tắc này mở rộng ra cả bên ngoài mã của bạn. Tất cả phần phụ thuộc của bạn đều phải sử dụng cùng một STL mà bạn đã chọn. Nếu phụ thuộc vào một phần phụ thuộc nguồn đóng của bên thứ ba có sử dụng STL và không cung cấp một thư viện cho mỗi STL, thì bạn không có lựa chọn STL nào. Bạn phải sử dụng cùng STL làm phần phụ thuộc.

Có thể bạn sẽ phụ thuộc vào hai thư viện không tương thích lẫn nhau. Trong trường hợp này, giải pháp duy nhất là loại bỏ một trong những phần phụ thuộc đó hoặc yêu cầu người duy trì cung cấp một thư viện được xây dựng dựa trên STL còn lại.

Ngoại lệ trong C++

Các ngoại lệ trong C++ được libc++ hỗ trợ. Nhưng trong ndk-build, các ngoại lệ này bị tắt theo mặc định. Lý do là các ngoại lệ trong C++ trước đây không có trong NDK. CMake và các chuỗi công cụ độc lập có các ngoại lệ trong C++ được bật theo mặc định.

Để áp dụng các ngoại lệ trên toàn bộ ứng dụng trong ndk-build, hãy thêm dòng sau đây vào tệp Application.mk:

APP_CPPFLAGS := -fexceptions

Để áp dụng các ngoại lệ cho một mô-đun ndk-build duy nhất, hãy thêm dòng sau đây vào phần tương ứng trong Android.mk của mô-đun đó:

LOCAL_CPP_FEATURES := exceptions

Ngoài ra, bạn có thể sử dụng:

LOCAL_CPPFLAGS := -fexceptions

RTTI

Giống như các ngoại lệ, libc++ có hỗ trợ RTTI (thông tin kiểu thời gian chạy) nhưng chế độ này bị tắt theo mặc định trong ndk-build. CMake và các chuỗi công cụ độc lập bật RTTI theo mặc định.

Để bật RTTI trên toàn bộ ứng dụng trong ndk-build, hãy thêm dòng sau đây vào tệp Application.mk:

APP_CPPFLAGS := -frtti

Để bật RTTI cho một mô-đun ndk-build duy nhất, hãy thêm dòng sau đây vào Android.mk của mô-đun đó:

LOCAL_CPP_FEATURES := rtti

Ngoài ra, bạn có thể sử dụng:

LOCAL_CPPFLAGS := -frtti