Skip to content

Commit e1e3173

Browse files
committed
README for "Firebird embedded usage in an Android app using C++"
1 parent 4c3f065 commit e1e3173

File tree

1 file changed

+363
-0
lines changed

1 file changed

+363
-0
lines changed

android-cpp/README.md

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
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

Comments
 (0)