|
| 1 | +# Firebird embedded usage in an Android app using C++ |
| 2 | + |
| 3 | +This example shows how to use Firebird embedded in an Android app using C++. |
| 4 | + |
| 5 | +The app is initialized using Kotlin and talks with a native C++ module that talks to Firebird. |
| 6 | + |
| 7 | +As a first step, create an Android project using Android Studio. |
| 8 | + |
| 9 | +Application has been created using `targetSdk 32` but we need to change it to `targetSdk 33`: |
| 10 | + |
| 11 | +```diff |
| 12 | +commit 7dfecc8af7f9f310c3f4b621c204dde01652aa82 |
| 13 | +Author: Adriano dos Santos Fernandes < [email protected]> |
| 14 | +Date: Wed Feb 15 21:42:28 2023 -0300 |
| 15 | + |
| 16 | + Set compileSdk to 33. |
| 17 | + |
| 18 | +diff --git a/android-cpp/app/build.gradle b/android-cpp/app/build.gradle |
| 19 | +index 5aa45e5..10a1fdd 100644 |
| 20 | +--- a/android-cpp/app/build.gradle |
| 21 | ++++ b/android-cpp/app/build.gradle |
| 22 | +@@ -4,7 +4,7 @@ plugins { |
| 23 | + } |
| 24 | + |
| 25 | + android { |
| 26 | +- compileSdk 32 |
| 27 | ++ compileSdk 33 |
| 28 | + |
| 29 | + defaultConfig { |
| 30 | + applicationId "com.example.firebirdandroidcpp" |
| 31 | +``` |
| 32 | + |
| 33 | +Then we need to download a `Firebird embedded AAR` - an Android archive with Firebird libraries compiled to the four Android ABIs (armeabi-v7a, arm64-v8a, x86, x86_64). |
| 34 | + |
| 35 | +Currently only Firebird 5 beta snapshots are bundled as AAR and they can be downloaded from https://github.com/FirebirdSQL/snapshots/releases/tag/snapshot-master. |
| 36 | +Don't rely on them for important work. |
| 37 | + |
| 38 | +We will download the file and save as `android-cpp/app/libs/Firebird-5.0.0-android-embedded.aar`. |
| 39 | + |
| 40 | +We need to reference this file in Android app' `build.gradle` file: |
| 41 | + |
| 42 | +```diff |
| 43 | +commit 151d490dc86bf74990e0558117b45826e633f410 |
| 44 | +Author: Adriano dos Santos Fernandes < [email protected]> |
| 45 | +Date: Wed Feb 15 21:42:28 2023 -0300 |
| 46 | + |
| 47 | + Reference libs/Firebird-5.0.0-android-embedded.aar. |
| 48 | + |
| 49 | +diff --git a/android-cpp/app/build.gradle b/android-cpp/app/build.gradle |
| 50 | +index 10a1fdd..6991758 100644 |
| 51 | +--- a/android-cpp/app/build.gradle |
| 52 | ++++ b/android-cpp/app/build.gradle |
| 53 | +@@ -54,4 +54,6 @@ dependencies { |
| 54 | + testImplementation 'junit:junit:4.13.2' |
| 55 | + androidTestImplementation 'androidx.test.ext:junit:1.1.5' |
| 56 | + androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' |
| 57 | +-} |
| 58 | ++ |
| 59 | ++ implementation files('libs/Firebird-5.0.0-android-embedded.aar') |
| 60 | ++} |
| 61 | +``` |
| 62 | + |
| 63 | +Then we will grab Firebird's `include` directory and put in `android-cpp/app/src/main/cpp/include`. |
| 64 | +These files are not present in the `AAR` file, so it must be get from a non-Android kit. |
| 65 | + |
| 66 | +We are going to configure `cmake` to treat this directory as an `include` directory: |
| 67 | + |
| 68 | +```diff |
| 69 | +commit c8a78dbf6cebba57babf47378c8eb69529808aad |
| 70 | +Author: Adriano dos Santos Fernandes < [email protected]> |
| 71 | +Date: Wed Feb 15 21:42:28 2023 -0300 |
| 72 | + |
| 73 | + Add app/src/main/cpp/include to CMake include path. |
| 74 | + |
| 75 | +diff --git a/android-cpp/app/src/main/cpp/CMakeLists.txt b/android-cpp/app/src/main/cpp/CMakeLists.txt |
| 76 | +index d0ecd00..9233a7b 100644 |
| 77 | +--- a/android-cpp/app/src/main/cpp/CMakeLists.txt |
| 78 | ++++ b/android-cpp/app/src/main/cpp/CMakeLists.txt |
| 79 | +@@ -36,6 +36,10 @@ find_library( # Sets the name of the path variable. |
| 80 | + # you want CMake to locate. |
| 81 | + log) |
| 82 | + |
| 83 | ++target_include_directories( |
| 84 | ++ firebirdandroidcpp PUBLIC |
| 85 | ++ $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>) |
| 86 | ++ |
| 87 | + # Specifies libraries CMake should link to your target library. You |
| 88 | + # can link multiple libraries, such as libraries you define in this |
| 89 | + # build script, prebuilt third-party libraries, or system libraries. |
| 90 | +``` |
| 91 | + |
| 92 | +We are then going to change the scaffolded app with the code we need. |
| 93 | + |
| 94 | +In `android-cpp/app/src/main/java/com/example/firebirdandroidcpp/MainActivity.kt` we need to put our code in the `MainActivity.onCreate` method, that's going to be as this: |
| 95 | + |
| 96 | +```kotlin |
| 97 | +override fun onCreate(savedInstanceState: Bundle?) { |
| 98 | + super.onCreate(savedInstanceState) |
| 99 | + |
| 100 | + FirebirdConf.extractAssets(baseContext, false) |
| 101 | + FirebirdConf.setEnv(baseContext) |
| 102 | + |
| 103 | + connect(File(filesDir, "test.fdb").absolutePath) |
| 104 | + |
| 105 | + binding = ActivityMainBinding.inflate(layoutInflater) |
| 106 | + setContentView(binding.root) |
| 107 | + |
| 108 | + try { |
| 109 | + binding.sampleText.text = getCurrentTimestamp() |
| 110 | + } |
| 111 | + catch (e: Exception) { |
| 112 | + binding.sampleText.text = "Error: ${e.message}" |
| 113 | + } |
| 114 | +} |
| 115 | +``` |
| 116 | + |
| 117 | +The `FirebirdConf` class (imported from `org.firebirdsql.android.embedded.FirebirdConf`) has helper functions to setup Firebird usage in Android. |
| 118 | + |
| 119 | +`FirebirdConf.extractAssets` extracts bundled config and data files as Firebird cannot work with them bundled. `FirebirdConf.setEnv` sets necessary environment variables so Firebird know where these files are. |
| 120 | + |
| 121 | +`connect(File(filesDir, "test.fdb").absolutePath)` is the call for the native C++ method we are going to create that connects to the database or create it when it does not exist. |
| 122 | + |
| 123 | +The `binding.sampleText.text = getCurrentTimestamp()` line gets the `CURRENT_TIMESTAMP` using a Firebird query. |
| 124 | + |
| 125 | +It's also good to release resources, so we are going to create an `onDestroy` method that calls our own `disconnect` native C++ method: |
| 126 | + |
| 127 | +```kotlin |
| 128 | +override fun onDestroy() { |
| 129 | + disconnect(); |
| 130 | + super.onDestroy() |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +As a final step in the Kotlin file, we need to declare the external C++ methods: |
| 135 | + |
| 136 | +```kotlin |
| 137 | +private external fun connect(databaseName: String) |
| 138 | +private external fun disconnect() |
| 139 | +private external fun getCurrentTimestamp(): String |
| 140 | +``` |
| 141 | + |
| 142 | +Now we are going to put the C++ code that is in the middle between the Kotlin app and Firebird. |
| 143 | + |
| 144 | +This file has nothing very special. It's just standard C++ code that interfaces with JNI and also with Firebird. |
| 145 | + |
| 146 | +Since Firebird raw API is not very ease to use, others libraries may be used too. |
| 147 | + |
| 148 | +We start with the C++ headers: |
| 149 | + |
| 150 | +```c++ |
| 151 | +#include <jni.h> |
| 152 | +#include <exception> |
| 153 | +#include <memory> |
| 154 | +#include <stdexcept> |
| 155 | +#include <string> |
| 156 | +#include <dlfcn.h> |
| 157 | +#include "firebird/Interface.h" |
| 158 | +#include "firebird/Message.h" |
| 159 | +``` |
| 160 | + |
| 161 | +Then some things to make easier to use the Firebird library: |
| 162 | + |
| 163 | +```c++ |
| 164 | +namespace fb = Firebird; |
| 165 | + |
| 166 | +using GetMasterPtr = decltype(&fb::fb_get_master_interface); |
| 167 | + |
| 168 | +static constexpr auto LIB_FBCLIENT = "libfbclient.so"; |
| 169 | +static constexpr auto SYMBOL_GET_MASTER_INTERFACE = "fb_get_master_interface"; |
| 170 | +``` |
| 171 | +
|
| 172 | +And the global variables that stores the loaded library and active connection: |
| 173 | +
|
| 174 | +```c++ |
| 175 | +static void* handle = nullptr; |
| 176 | +static GetMasterPtr masterFunc = nullptr; |
| 177 | +static fb::IMaster* master = nullptr; |
| 178 | +static fb::IUtil* util = nullptr; |
| 179 | +static fb::IProvider* dispatcher = nullptr; |
| 180 | +static fb::IStatus* status = nullptr; |
| 181 | +static std::unique_ptr<fb::ThrowStatusWrapper> statusWrapper; |
| 182 | +static fb::IAttachment* attachment = nullptr; |
| 183 | +``` |
| 184 | + |
| 185 | +The more important functions are `loadLibrary`, `unloadLibrary`, `connect`, `disconnect` and `getCurrentTimestamp`, that are the code indirectly called by the Kotlin external methods and actually interface with Firebird. These functions are coded in a way that they do not deal with JNI: |
| 186 | + |
| 187 | +```c++ |
| 188 | +// Loads Firebird library and get main interfaces. |
| 189 | +static void loadLibrary() { |
| 190 | + if (handle) |
| 191 | + return; |
| 192 | + |
| 193 | + if (!(handle = dlopen(LIB_FBCLIENT, RTLD_NOW))) |
| 194 | + throw std::runtime_error("Error loading Firebird client library."); |
| 195 | + |
| 196 | + if (!(masterFunc = (GetMasterPtr) dlsym(handle, SYMBOL_GET_MASTER_INTERFACE))) { |
| 197 | + dlclose(handle); |
| 198 | + handle = nullptr; |
| 199 | + throw std::runtime_error("Error getting Firebird master interface."); |
| 200 | + } |
| 201 | + |
| 202 | + master = masterFunc(); |
| 203 | + util = master->getUtilInterface(); |
| 204 | + dispatcher = master->getDispatcher(); |
| 205 | + status = master->getStatus(); |
| 206 | + statusWrapper = std::make_unique<fb::ThrowStatusWrapper>(status); |
| 207 | +} |
| 208 | + |
| 209 | +// Unloads Firebird library. |
| 210 | +static void unloadLibrary() { |
| 211 | + if (handle) { |
| 212 | + dispatcher->shutdown(statusWrapper.get(), 0, fb_shutrsn_app_stopped); |
| 213 | + status->dispose(); |
| 214 | + dispatcher->release(); |
| 215 | + dlclose(handle); |
| 216 | + handle = nullptr; |
| 217 | + } |
| 218 | +} |
| 219 | + |
| 220 | +// Connects to Firebird database. Creates it if necessary. |
| 221 | +static void connect(std::string databaseName) { |
| 222 | + loadLibrary(); |
| 223 | + |
| 224 | + try { |
| 225 | + attachment = dispatcher->attachDatabase( |
| 226 | + statusWrapper.get(), |
| 227 | + databaseName.c_str(), |
| 228 | + 0, |
| 229 | + nullptr); |
| 230 | + } |
| 231 | + catch (const fb::FbException&) { |
| 232 | + attachment = dispatcher->createDatabase( |
| 233 | + statusWrapper.get(), |
| 234 | + databaseName.c_str(), |
| 235 | + 0, |
| 236 | + nullptr); |
| 237 | + } |
| 238 | +} |
| 239 | + |
| 240 | +// Disconnects the database. |
| 241 | +static void disconnect() { |
| 242 | + if (attachment) { |
| 243 | + attachment->detach(statusWrapper.get()); |
| 244 | + attachment->release(); |
| 245 | + attachment = nullptr; |
| 246 | + } |
| 247 | + |
| 248 | + unloadLibrary(); |
| 249 | +} |
| 250 | + |
| 251 | +// Query CURRENT_TIMESTAMP using a Firebird query. |
| 252 | +static std::string getCurrentTimestamp() { |
| 253 | + const auto transaction = attachment->startTransaction(statusWrapper.get(), 0, nullptr); |
| 254 | + |
| 255 | + FB_MESSAGE(message, fb::ThrowStatusWrapper, |
| 256 | + (FB_VARCHAR(64), currentTimestamp) |
| 257 | + ) message(statusWrapper.get(), master); |
| 258 | + |
| 259 | + attachment->execute( |
| 260 | + statusWrapper.get(), |
| 261 | + transaction, |
| 262 | + 0, |
| 263 | + "select current_timestamp from rdb$database", |
| 264 | + SQL_DIALECT_CURRENT, |
| 265 | + nullptr, |
| 266 | + nullptr, |
| 267 | + message.getMetadata(), |
| 268 | + message.getData()); |
| 269 | + |
| 270 | + transaction->commit(statusWrapper.get()); |
| 271 | + transaction->release(); |
| 272 | + |
| 273 | + return std::string(message->currentTimestamp.str, message->currentTimestamp.length); |
| 274 | +} |
| 275 | +``` |
| 276 | +
|
| 277 | +The functions (`Java_com_example_firebirdandroidcpp_MainActivity_*`) are the directly ones called by the Kotlin code and they deal with JNI-specific types and exceptions, passing actual work for the above functions: |
| 278 | +
|
| 279 | +```c++ |
| 280 | +// JNI JMainActivity.connect. |
| 281 | +extern "C" JNIEXPORT |
| 282 | +void JNICALL Java_com_example_firebirdandroidcpp_MainActivity_connect( |
| 283 | + JNIEnv* env, jobject self, jstring databaseName) { |
| 284 | + try { |
| 285 | + connect(convertJString(env, databaseName)); |
| 286 | + } |
| 287 | + catch (...) { |
| 288 | + jniRethrow(env); |
| 289 | + } |
| 290 | +} |
| 291 | +
|
| 292 | +// JNI JMainActivity.disconnect. |
| 293 | +extern "C" JNIEXPORT |
| 294 | +void JNICALL Java_com_example_firebirdandroidcpp_MainActivity_disconnect( |
| 295 | + JNIEnv* env, jobject self) { |
| 296 | + try { |
| 297 | + disconnect(); |
| 298 | + } |
| 299 | + catch (...) { |
| 300 | + jniRethrow(env); |
| 301 | + } |
| 302 | +} |
| 303 | +
|
| 304 | +// JNI JMainActivity.getCurrentTimestamp. |
| 305 | +extern "C" JNIEXPORT |
| 306 | +jstring JNICALL Java_com_example_firebirdandroidcpp_MainActivity_getCurrentTimestamp( |
| 307 | + JNIEnv* env, jobject self) { |
| 308 | + try { |
| 309 | + std::string currentTimestamp = getCurrentTimestamp(); |
| 310 | + return env->NewStringUTF(currentTimestamp.c_str()); |
| 311 | + } |
| 312 | + catch (...) { |
| 313 | + jniRethrow(env); |
| 314 | + return nullptr; |
| 315 | + } |
| 316 | +} |
| 317 | +``` |
| 318 | + |
| 319 | +The only missing piece of native code is the JNI helper functions. |
| 320 | +`convertJString` converts JNI string to `std::string` and `jniRethrow` converts C++ exception to Android exceptions: |
| 321 | + |
| 322 | +```c++ |
| 323 | +// Converts JNI string to std::string. |
| 324 | +static std::string convertJString(JNIEnv* env, jstring str) { |
| 325 | + if (!str) |
| 326 | + return {}; |
| 327 | + |
| 328 | + const auto len = env->GetStringUTFLength(str); |
| 329 | + const auto strChars = env->GetStringUTFChars(str, nullptr); |
| 330 | + |
| 331 | + std::string result(strChars, len); |
| 332 | + |
| 333 | + env->ReleaseStringUTFChars(str, strChars); |
| 334 | + |
| 335 | + return result; |
| 336 | +} |
| 337 | + |
| 338 | +// Rethrow C++ as JNI exception. |
| 339 | +static void jniRethrow(JNIEnv* env) |
| 340 | +{ |
| 341 | + std::string message; |
| 342 | + |
| 343 | + try { |
| 344 | + throw; |
| 345 | + assert(false); |
| 346 | + return; |
| 347 | + } |
| 348 | + catch (const fb::FbException& e) { |
| 349 | + char buffer[1024]; |
| 350 | + util->formatStatus(buffer, sizeof(buffer), e.getStatus()); |
| 351 | + message = buffer; |
| 352 | + } |
| 353 | + catch (const std::exception& e) { |
| 354 | + message = e.what(); |
| 355 | + } |
| 356 | + catch (...) { |
| 357 | + message = "Unrecognized C++ exception"; |
| 358 | + } |
| 359 | + |
| 360 | + const auto exception = env->FindClass("java/lang/Exception"); |
| 361 | + env->ThrowNew(exception, message.c_str()); |
| 362 | +} |
| 363 | +``` |
0 commit comments