Chapter 2: Data Types & Type Conversion
Introduction
In Chapter 1, you built your first NDK application and learned how JNI bridges Kotlin and C++. But we only scratched the surface of data passing — we returned a simple string. Real applications need to exchange numbers, text, and complex data structures between the two worlds.
This chapter is your complete guide to data type mapping and conversion in JNI. You'll master primitive types, string handling, and the critical encoding considerations that can make or break your application. By the end, you'll build a Secure Text Encoder that implements multiple encoding algorithms entirely in native code.
Primitive Type Mapping
JNI defines its own type system that acts as an intermediary between Kotlin/Java types and C/C++ types. Understanding this mapping is fundamental to NDK development.
The Complete Type Mapping Table
| Kotlin Type | JNI Type | C/C++ Type | Size | Range |
|---|---|---|---|---|
Boolean |
jboolean |
uint8_t |
8 bits | JNI_TRUE (1) or JNI_FALSE (0) |
Byte |
jbyte |
int8_t |
8 bits | -128 to 127 |
Char |
jchar |
uint16_t |
16 bits | 0 to 65535 (Unicode) |
Short |
jshort |
int16_t |
16 bits | -32,768 to 32,767 |
Int |
jint |
int32_t |
32 bits | -2³¹ to 2³¹-1 |
Long |
jlong |
int64_t |
64 bits | -2⁶³ to 2⁶³-1 |
Float |
jfloat |
float |
32 bits | IEEE 754 single |
Double |
jdouble |
double |
64 bits | IEEE 754 double |
Important Type Considerations
1. Boolean Handling
// jboolean is unsigned 8-bit
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_TypeDemo_checkEven(JNIEnv* env, jobject thiz, jint number) {
// Use JNI_TRUE and JNI_FALSE for clarity
return (number % 2 == 0) ? JNI_TRUE : JNI_FALSE;
}
2. Character Encoding
// jchar is 16-bit UTF-16, NOT 8-bit ASCII
extern "C" JNIEXPORT jchar JNICALL
Java_com_example_TypeDemo_toUpperCase(JNIEnv* env, jobject thiz, jchar ch) {
// Works for ASCII range
if (ch >= 'a' && ch <= 'z') {
return ch - 32;
}
return ch;
}
3. Size Guarantees Unlike native C/C++ types, JNI types have guaranteed sizes across all platforms:
// C/C++ 'int' size varies by platform (16, 32, or 64 bits)
// jint is ALWAYS 32 bits
void demonstrateSizes() {
static_assert(sizeof(jbyte) == 1, "jbyte must be 1 byte");
static_assert(sizeof(jshort) == 2, "jshort must be 2 bytes");
static_assert(sizeof(jint) == 4, "jint must be 4 bytes");
static_assert(sizeof(jlong) == 8, "jlong must be 8 bytes");
static_assert(sizeof(jfloat) == 4, "jfloat must be 4 bytes");
static_assert(sizeof(jdouble) == 8, "jdouble must be 8 bytes");
}
Passing Primitives to Native Code
Passing primitives from Kotlin to C++ is straightforward — JNI handles the conversion automatically.
Basic Parameter Passing
// Kotlin
class Calculator {
external fun add(a: Int, b: Int): Int
external fun multiply(a: Double, b: Double): Double
external fun isPositive(number: Long): Boolean
}
// C++
extern "C" JNIEXPORT jint JNICALL
Java_com_example_Calculator_add(
JNIEnv* env,
jobject thiz,
jint a,
jint b
) {
return a + b;
}
extern "C" JNIEXPORT jdouble JNICALL
Java_com_example_Calculator_multiply(
JNIEnv* env,
jobject thiz,
jdouble a,
jdouble b
) {
return a * b;
}
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_Calculator_isPositive(
JNIEnv* env,
jobject thiz,
jlong number
) {
return number > 0 ? JNI_TRUE : JNI_FALSE;
}
Multiple Parameters
// Kotlin
external fun computeFormula(a: Int, b: Float, c: Double, flag: Boolean): Double
// C++
extern "C" JNIEXPORT jdouble JNICALL
Java_com_example_Calculator_computeFormula(
JNIEnv* env,
jobject thiz,
jint a,
jfloat b,
jdouble c,
jboolean flag
) {
if (flag) {
return static_cast<jdouble>(a) * b + c;
} else {
return static_cast<jdouble>(a) * b - c;
}
}
Returning Values from Native Functions
Returning primitives follows the same automatic conversion pattern.
Return Type Examples
// Returning different types
extern "C" JNIEXPORT jbyte JNICALL
Java_com_example_Demo_getMaxByte(JNIEnv* env, jobject thiz) {
return static_cast<jbyte>(127); // Max signed byte value
}
extern "C" JNIEXPORT jshort JNICALL
Java_com_example_Demo_getPortNumber(JNIEnv* env, jobject thiz) {
return static_cast<jshort>(8080);
}
extern "C" JNIEXPORT jint JNICALL
Java_com_example_Demo_getStatusCode(JNIEnv* env, jobject thiz) {
return 200;
}
extern "C" JNIEXPORT jlong JNICALL
Java_com_example_Demo_getTimestamp(JNIEnv* env, jobject thiz) {
// Return current time in milliseconds
auto now = std::chrono::system_clock::now();
auto duration = now.time_since_epoch();
return std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
}
extern "C" JNIEXPORT jfloat JNICALL
Java_com_example_Demo_getPi(JNIEnv* env, jobject thiz) {
return 3.14159265f;
}
extern "C" JNIEXPORT jdouble JNICALL
Java_com_example_Demo_getE(JNIEnv* env, jobject thiz) {
return 2.71828182845904523536;
}
Handling Overflow and Precision
// Be careful with overflow
extern "C" JNIEXPORT jint JNICALL
Java_com_example_SafeMath_safeAdd(
JNIEnv* env,
jobject thiz,
jint a,
jint b
) {
// Check for overflow before addition
if (a > 0 && b > INT32_MAX - a) {
// Positive overflow would occur
return INT32_MAX;
}
if (a < 0 && b < INT32_MIN - a) {
// Negative overflow would occur
return INT32_MIN;
}
return a + b;
}
// Be careful with float/double precision
extern "C" JNIEXPORT jboolean JNICALL
Java_com_example_SafeMath_areEqual(
JNIEnv* env,
jobject thiz,
jdouble a,
jdouble b,
jdouble epsilon
) {
// Never compare floats/doubles with ==
return std::abs(a - b) < epsilon ? JNI_TRUE : JNI_FALSE;
}
Working with Strings: jstring and UTF-8 Encoding
Strings are where JNI gets interesting. Unlike primitives, strings require explicit conversion and memory management.
Understanding String Encoding
- Kotlin/Java strings: UTF-16 encoded internally
- JNI Modified UTF-8: A slightly modified UTF-8 encoding used by JNI
- C++ strings: Typically char* (ASCII or UTF-8) or wchar_t* (wide characters)
The jstring Type
jstring is not a C string — it's an opaque reference to a Java String object. You cannot directly access its contents.
Converting jstring to C++ String
JNI provides two main functions for string conversion:
1. GetStringUTFChars / ReleaseStringUTFChars
extern "C" JNIEXPORT jint JNICALL
Java_com_example_StringDemo_getStringLength(
JNIEnv* env,
jobject thiz,
jstring input
) {
// Get a pointer to the UTF-8 characters
const char* nativeString = env->GetStringUTFChars(input, nullptr);
if (nativeString == nullptr) {
// Out of memory
return -1;
}
// Now we can use it as a C string
int length = strlen(nativeString);
// CRITICAL: Always release when done!
env->ReleaseStringUTFChars(input, nativeString);
return length;
}
2. GetStringCritical / ReleaseStringCritical
// For performance-critical code when you need direct access
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_StringDemo_processStringFast(
JNIEnv* env,
jobject thiz,
jstring input
) {
// Get critical access (may return UTF-16)
jboolean isCopy;
const jchar* chars = env->GetStringCritical(input, &isCopy);
if (chars == nullptr) {
return env->NewStringUTF("Error: Could not access string");
}
jsize length = env->GetStringLength(input);
// Process UTF-16 characters...
// Note: Between GetStringCritical and ReleaseStringCritical,
// you CANNOT call other JNI functions!
env->ReleaseStringCritical(input, chars);
return env->NewStringUTF("Processed");
}
GetStringUTFChars vs GetStringCritical
| Aspect | GetStringUTFChars | GetStringCritical |
|---|---|---|
| Encoding | Modified UTF-8 | Platform native (often UTF-16) |
| May copy | Yes (set isCopy) | Tries to avoid copy |
| During access | Can call JNI functions | CANNOT call JNI functions |
| Best for | General use | Performance-critical loops |
| Safety | Safer | Must be very careful |
Converting C++ String to jstring
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_StringDemo_createGreeting(
JNIEnv* env,
jobject thiz,
jstring name
) {
const char* nameChars = env->GetStringUTFChars(name, nullptr);
if (nameChars == nullptr) {
return nullptr;
}
// Build a C++ string
std::string greeting = "Hello, ";
greeting += nameChars;
greeting += "! Welcome to NDK.";
// Release the input string
env->ReleaseStringUTFChars(name, nameChars);
// Create and return a new Java string
return env->NewStringUTF(greeting.c_str());
}
String Helper Functions
Here's a utility class for safer string handling:
// StringHelper.h
#ifndef STRING_HELPER_H
#define STRING_HELPER_H
#include <jni.h>
#include <string>
class JniString {
public:
JniString(JNIEnv* env, jstring jstr)
: env_(env), jstr_(jstr), chars_(nullptr) {
if (jstr != nullptr) {
chars_ = env->GetStringUTFChars(jstr, nullptr);
}
}
~JniString() {
if (chars_ != nullptr) {
env_->ReleaseStringUTFChars(jstr_, chars_);
}
}
// Delete copy constructor and assignment
JniString(const JniString&) = delete;
JniString& operator=(const JniString&) = delete;
const char* c_str() const { return chars_; }
std::string str() const { return chars_ ? std::string(chars_) : ""; }
bool isValid() const { return chars_ != nullptr; }
private:
JNIEnv* env_;
jstring jstr_;
const char* chars_;
};
#endif
Usage:
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_StringDemo_processString(
JNIEnv* env,
jobject thiz,
jstring input
) {
JniString str(env, input);
if (!str.isValid()) {
return env->NewStringUTF("Error");
}
std::string result = "Processed: " + str.str();
return env->NewStringUTF(result.c_str());
// JniString destructor automatically releases memory
}
String Manipulation in Native Code
Let's explore common string operations in native code.
String Concatenation
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_StringOps_concatenate(
JNIEnv* env,
jobject thiz,
jstring str1,
jstring str2
) {
const char* s1 = env->GetStringUTFChars(str1, nullptr);
const char* s2 = env->GetStringUTFChars(str2, nullptr);
if (s1 == nullptr || s2 == nullptr) {
if (s1) env->ReleaseStringUTFChars(str1, s1);
if (s2) env->ReleaseStringUTFChars(str2, s2);
return nullptr;
}
std::string result = std::string(s1) + s2;
env->ReleaseStringUTFChars(str1, s1);
env->ReleaseStringUTFChars(str2, s2);
return env->NewStringUTF(result.c_str());
}
String Search
extern "C" JNIEXPORT jint JNICALL
Java_com_example_StringOps_indexOf(
JNIEnv* env,
jobject thiz,
jstring haystack,
jstring needle
) {
JniString h(env, haystack);
JniString n(env, needle);
if (!h.isValid() || !n.isValid()) {
return -1;
}
const char* found = strstr(h.c_str(), n.c_str());
if (found == nullptr) {
return -1;
}
return static_cast<jint>(found - h.c_str());
}
String Transformation
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_StringOps_toUpperCase(
JNIEnv* env,
jobject thiz,
jstring input
) {
const char* inputChars = env->GetStringUTFChars(input, nullptr);
if (inputChars == nullptr) {
return nullptr;
}
std::string result(inputChars);
env->ReleaseStringUTFChars(input, inputChars);
// Transform to uppercase
std::transform(result.begin(), result.end(), result.begin(),
[](unsigned char c) { return std::toupper(c); });
return env->NewStringUTF(result.c_str());
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_StringOps_reverse(
JNIEnv* env,
jobject thiz,
jstring input
) {
const char* inputChars = env->GetStringUTFChars(input, nullptr);
if (inputChars == nullptr) {
return nullptr;
}
std::string result(inputChars);
env->ReleaseStringUTFChars(input, inputChars);
std::reverse(result.begin(), result.end());
return env->NewStringUTF(result.c_str());
}
Memory Considerations with Strings
String handling is a common source of memory leaks in JNI code. Let's understand the rules.
The Golden Rules
- Every GetStringUTFChars must have a matching ReleaseStringUTFChars
- Every GetStringCritical must have a matching ReleaseStringCritical
- NewStringUTF creates a new Java object — don't free it manually
- Return paths must all release resources
Common Memory Leak Pattern
// BAD: Memory leak on error path
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_BadExample_process(JNIEnv* env, jobject thiz, jstring input) {
const char* str = env->GetStringUTFChars(input, nullptr);
if (someErrorCondition) {
return nullptr; // LEAK! str was never released
}
// ... processing ...
env->ReleaseStringUTFChars(input, str);
return env->NewStringUTF("result");
}
Correct Pattern
// GOOD: Always release on all paths
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_GoodExample_process(JNIEnv* env, jobject thiz, jstring input) {
const char* str = env->GetStringUTFChars(input, nullptr);
if (str == nullptr) {
return nullptr; // OK: Nothing to release
}
jstring result;
if (someErrorCondition) {
result = env->NewStringUTF("error");
} else {
// ... processing ...
result = env->NewStringUTF("success");
}
env->ReleaseStringUTFChars(input, str); // Always release
return result;
}
RAII Pattern (Best Practice)
Use the JniString helper class shown earlier, or use C++11 smart pointers with custom deleters:
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_RaiiExample_process(JNIEnv* env, jobject thiz, jstring input) {
// Using unique_ptr with custom deleter
struct StringReleaser {
JNIEnv* env;
jstring jstr;
void operator()(const char* str) {
env->ReleaseStringUTFChars(jstr, str);
}
};
const char* rawStr = env->GetStringUTFChars(input, nullptr);
std::unique_ptr<const char, StringReleaser> str(
rawStr,
StringReleaser{env, input}
);
if (!str) {
return nullptr;
}
// Now you can return early without worrying about leaks
if (strlen(str.get()) == 0) {
return env->NewStringUTF("empty");
}
std::string result = "Processed: ";
result += str.get();
return env->NewStringUTF(result.c_str());
// str is automatically released when it goes out of scope
}
Type Safety and Common Pitfalls
Pitfall 1: Implicit Type Conversion
// BAD: Silent precision loss
extern "C" JNIEXPORT jint JNICALL
Java_com_example_Pitfalls_badConversion(JNIEnv* env, jobject thiz, jlong bigNumber) {
return bigNumber; // Truncates to 32 bits without warning!
}
// GOOD: Explicit handling
extern "C" JNIEXPORT jint JNICALL
Java_com_example_Pitfalls_goodConversion(JNIEnv* env, jobject thiz, jlong bigNumber) {
if (bigNumber > INT32_MAX || bigNumber < INT32_MIN) {
// Handle overflow - throw exception or return error value
return -1;
}
return static_cast<jint>(bigNumber);
}
Pitfall 2: Null Pointer Dereference
// BAD: No null check
extern "C" JNIEXPORT jint JNICALL
Java_com_example_Pitfalls_badNullHandling(JNIEnv* env, jobject thiz, jstring input) {
const char* str = env->GetStringUTFChars(input, nullptr);
int len = strlen(str); // CRASH if input was null!
env->ReleaseStringUTFChars(input, str);
return len;
}
// GOOD: Proper null checking
extern "C" JNIEXPORT jint JNICALL
Java_com_example_Pitfalls_goodNullHandling(JNIEnv* env, jobject thiz, jstring input) {
if (input == nullptr) {
return -1;
}
const char* str = env->GetStringUTFChars(input, nullptr);
if (str == nullptr) {
return -1;
}
int len = strlen(str);
env->ReleaseStringUTFChars(input, str);
return len;
}
Pitfall 3: String Encoding Issues
// BAD: Assuming ASCII
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_Pitfalls_badEncoding(JNIEnv* env, jobject thiz, jstring input) {
const char* str = env->GetStringUTFChars(input, nullptr);
// This breaks with non-ASCII characters!
char buffer[256];
for (int i = 0; str[i] && i < 255; i++) {
buffer[i] = toupper((unsigned char)str[i]);
buffer[i+1] = '\0';
}
env->ReleaseStringUTFChars(input, str);
return env->NewStringUTF(buffer);
}
// BETTER: Use UTF-8 aware libraries or handle multi-byte
// For full Unicode support, consider using ICU library
Pitfall 4: Boolean Comparison
// BAD: Direct comparison with true
extern "C" JNIEXPORT void JNICALL
Java_com_example_Pitfalls_badBooleanCheck(JNIEnv* env, jobject thiz, jboolean flag) {
// jboolean can be any non-zero value for true
if (flag == true) { // Might fail if flag is 2, 3, etc.
// ...
}
}
// GOOD: Check for non-false
extern "C" JNIEXPORT void JNICALL
Java_com_example_Pitfalls_goodBooleanCheck(JNIEnv* env, jobject thiz, jboolean flag) {
if (flag != JNI_FALSE) { // or simply: if (flag)
// ...
}
}
Project: Secure Text Encoder
Now let's build a practical application that demonstrates all the concepts from this chapter. We'll create a Secure Text Encoder with multiple encoding algorithms implemented in native code.
Project Overview
This app will implement:
- Base64 encoding/decoding
- ROT13 cipher
- Caesar cipher with configurable shift
- XOR encryption with a key
- URL encoding/decoding
All algorithms will be implemented in C++ for educational purposes and performance.
Project Structure
app/src/main/
├── cpp/
│ ├── CMakeLists.txt
│ ├── encoder.cpp
│ ├── encoder.h
│ ├── base64.cpp
│ ├── base64.h
│ └── jni_bridge.cpp
├── java/com/example/secureencoder/
│ ├── MainActivity.kt
│ ├── NativeEncoder.kt
│ └── ui/
│ ├── EncoderScreen.kt
│ └── components/
└── AndroidManifest.xml
Step 1: Create the Encoding Library
// encoder.h
#ifndef ENCODER_H
#define ENCODER_H
#include <string>
#include <vector>
namespace Encoder {
// Base64
std::string base64Encode(const std::string& input);
std::string base64Decode(const std::string& input);
// ROT13 (simple letter substitution)
std::string rot13(const std::string& input);
// Caesar cipher
std::string caesarEncrypt(const std::string& input, int shift);
std::string caesarDecrypt(const std::string& input, int shift);
// XOR encryption
std::string xorEncrypt(const std::string& input, const std::string& key);
std::string xorDecrypt(const std::string& input, const std::string& key);
// URL encoding
std::string urlEncode(const std::string& input);
std::string urlDecode(const std::string& input);
// Hex encoding
std::string toHex(const std::string& input);
std::string fromHex(const std::string& input);
}
#endif
// base64.cpp
#include "base64.h"
#include <stdexcept>
namespace {
const std::string BASE64_CHARS =
"ABCDEFGHIJKLMNOPQRSTUVWXYZ"
"abcdefghijklmnopqrstuvwxyz"
"0123456789+/";
bool isBase64(unsigned char c) {
return (isalnum(c) || (c == '+') || (c == '/'));
}
}
namespace Encoder {
std::string base64Encode(const std::string& input) {
std::string result;
int i = 0;
unsigned char char_array_3[3];
unsigned char char_array_4[4];
const unsigned char* bytes_to_encode =
reinterpret_cast<const unsigned char*>(input.c_str());
size_t in_len = input.length();
while (in_len--) {
char_array_3[i++] = *(bytes_to_encode++);
if (i == 3) {
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) +
((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) +
((char_array_3[2] & 0xc0) >> 6);
char_array_4[3] = char_array_3[2] & 0x3f;
for (i = 0; i < 4; i++) {
result += BASE64_CHARS[char_array_4[i]];
}
i = 0;
}
}
if (i) {
for (int j = i; j < 3; j++) {
char_array_3[j] = '\0';
}
char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
char_array_4[1] = ((char_array_3[0] & 0x03) << 4) +
((char_array_3[1] & 0xf0) >> 4);
char_array_4[2] = ((char_array_3[1] & 0x0f) << 2) +
((char_array_3[2] & 0xc0) >> 6);
for (int j = 0; j < i + 1; j++) {
result += BASE64_CHARS[char_array_4[j]];
}
while (i++ < 3) {
result += '=';
}
}
return result;
}
std::string base64Decode(const std::string& input) {
size_t in_len = input.size();
int i = 0;
int in_ = 0;
unsigned char char_array_4[4], char_array_3[3];
std::string result;
while (in_len-- && (input[in_] != '=') && isBase64(input[in_])) {
char_array_4[i++] = input[in_];
in_++;
if (i == 4) {
for (i = 0; i < 4; i++) {
char_array_4[i] = BASE64_CHARS.find(char_array_4[i]);
}
char_array_3[0] = (char_array_4[0] << 2) +
((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) +
((char_array_4[2] & 0x3c) >> 2);
char_array_3[2] = ((char_array_4[2] & 0x3) << 6) +
char_array_4[3];
for (i = 0; i < 3; i++) {
result += char_array_3[i];
}
i = 0;
}
}
if (i) {
for (int j = 0; j < i; j++) {
char_array_4[j] = BASE64_CHARS.find(char_array_4[j]);
}
char_array_3[0] = (char_array_4[0] << 2) +
((char_array_4[1] & 0x30) >> 4);
char_array_3[1] = ((char_array_4[1] & 0xf) << 4) +
((char_array_4[2] & 0x3c) >> 2);
for (int j = 0; j < i - 1; j++) {
result += char_array_3[j];
}
}
return result;
}
} // namespace Encoder
// encoder.cpp
#include "encoder.h"
#include <sstream>
#include <iomanip>
#include <cctype>
#include <algorithm>
namespace Encoder {
// ROT13 - rotates each letter by 13 positions
std::string rot13(const std::string& input) {
std::string result = input;
for (char& c : result) {
if (c >= 'a' && c <= 'z') {
c = 'a' + (c - 'a' + 13) % 26;
} else if (c >= 'A' && c <= 'Z') {
c = 'A' + (c - 'A' + 13) % 26;
}
// Non-alphabetic characters remain unchanged
}
return result;
}
// Caesar cipher with configurable shift
std::string caesarEncrypt(const std::string& input, int shift) {
// Normalize shift to 0-25 range
shift = ((shift % 26) + 26) % 26;
std::string result = input;
for (char& c : result) {
if (c >= 'a' && c <= 'z') {
c = 'a' + (c - 'a' + shift) % 26;
} else if (c >= 'A' && c <= 'Z') {
c = 'A' + (c - 'A' + shift) % 26;
}
}
return result;
}
std::string caesarDecrypt(const std::string& input, int shift) {
// Decryption is encryption with negative shift
return caesarEncrypt(input, -shift);
}
// XOR encryption
std::string xorEncrypt(const std::string& input, const std::string& key) {
if (key.empty()) {
return input;
}
std::string result = input;
size_t keyLen = key.length();
for (size_t i = 0; i < result.length(); i++) {
result[i] = result[i] ^ key[i % keyLen];
}
return result;
}
std::string xorDecrypt(const std::string& input, const std::string& key) {
// XOR decryption is the same as encryption
return xorEncrypt(input, key);
}
// URL encoding
std::string urlEncode(const std::string& input) {
std::ostringstream escaped;
escaped.fill('0');
escaped << std::hex;
for (char c : input) {
// Keep alphanumeric and unreserved characters
if (isalnum(static_cast<unsigned char>(c)) ||
c == '-' || c == '_' || c == '.' || c == '~') {
escaped << c;
} else if (c == ' ') {
escaped << '+';
} else {
escaped << std::uppercase;
escaped << '%' << std::setw(2) <<
int(static_cast<unsigned char>(c));
escaped << std::nouppercase;
}
}
return escaped.str();
}
std::string urlDecode(const std::string& input) {
std::string result;
for (size_t i = 0; i < input.length(); i++) {
if (input[i] == '%') {
if (i + 2 < input.length()) {
int value;
std::istringstream iss(input.substr(i + 1, 2));
if (iss >> std::hex >> value) {
result += static_cast<char>(value);
i += 2;
} else {
result += input[i];
}
} else {
result += input[i];
}
} else if (input[i] == '+') {
result += ' ';
} else {
result += input[i];
}
}
return result;
}
// Hex encoding
std::string toHex(const std::string& input) {
std::ostringstream result;
result << std::hex << std::setfill('0');
for (unsigned char c : input) {
result << std::setw(2) << static_cast<int>(c);
}
return result.str();
}
std::string fromHex(const std::string& input) {
std::string result;
if (input.length() % 2 != 0) {
return ""; // Invalid hex string
}
for (size_t i = 0; i < input.length(); i += 2) {
std::string byteString = input.substr(i, 2);
char byte = static_cast<char>(strtol(byteString.c_str(), nullptr, 16));
result += byte;
}
return result;
}
} // namespace Encoder
Step 2: Create the JNI Bridge
// jni_bridge.cpp
#include <jni.h>
#include <string>
#include "encoder.h"
#include "base64.h"
// Helper function to convert jstring to std::string
std::string jstringToString(JNIEnv* env, jstring jStr) {
if (jStr == nullptr) {
return "";
}
const char* chars = env->GetStringUTFChars(jStr, nullptr);
if (chars == nullptr) {
return "";
}
std::string result(chars);
env->ReleaseStringUTFChars(jStr, chars);
return result;
}
// Base64 encoding
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_base64Encode(
JNIEnv* env,
jobject /* this */,
jstring input
) {
std::string inputStr = jstringToString(env, input);
std::string result = Encoder::base64Encode(inputStr);
return env->NewStringUTF(result.c_str());
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_base64Decode(
JNIEnv* env,
jobject /* this */,
jstring input
) {
std::string inputStr = jstringToString(env, input);
std::string result = Encoder::base64Decode(inputStr);
return env->NewStringUTF(result.c_str());
}
// ROT13
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_rot13(
JNIEnv* env,
jobject /* this */,
jstring input
) {
std::string inputStr = jstringToString(env, input);
std::string result = Encoder::rot13(inputStr);
return env->NewStringUTF(result.c_str());
}
// Caesar cipher
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_caesarEncrypt(
JNIEnv* env,
jobject /* this */,
jstring input,
jint shift
) {
std::string inputStr = jstringToString(env, input);
std::string result = Encoder::caesarEncrypt(inputStr, shift);
return env->NewStringUTF(result.c_str());
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_caesarDecrypt(
JNIEnv* env,
jobject /* this */,
jstring input,
jint shift
) {
std::string inputStr = jstringToString(env, input);
std::string result = Encoder::caesarDecrypt(inputStr, shift);
return env->NewStringUTF(result.c_str());
}
// XOR encryption
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_xorEncrypt(
JNIEnv* env,
jobject /* this */,
jstring input,
jstring key
) {
std::string inputStr = jstringToString(env, input);
std::string keyStr = jstringToString(env, key);
std::string encrypted = Encoder::xorEncrypt(inputStr, keyStr);
// Return as hex since XOR result may contain non-printable characters
std::string result = Encoder::toHex(encrypted);
return env->NewStringUTF(result.c_str());
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_xorDecrypt(
JNIEnv* env,
jobject /* this */,
jstring input,
jstring key
) {
std::string inputStr = jstringToString(env, input);
std::string keyStr = jstringToString(env, key);
// Input is hex encoded
std::string decoded = Encoder::fromHex(inputStr);
std::string result = Encoder::xorDecrypt(decoded, keyStr);
return env->NewStringUTF(result.c_str());
}
// URL encoding
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_urlEncode(
JNIEnv* env,
jobject /* this */,
jstring input
) {
std::string inputStr = jstringToString(env, input);
std::string result = Encoder::urlEncode(inputStr);
return env->NewStringUTF(result.c_str());
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_urlDecode(
JNIEnv* env,
jobject /* this */,
jstring input
) {
std::string inputStr = jstringToString(env, input);
std::string result = Encoder::urlDecode(inputStr);
return env->NewStringUTF(result.c_str());
}
// Hex encoding
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_toHex(
JNIEnv* env,
jobject /* this */,
jstring input
) {
std::string inputStr = jstringToString(env, input);
std::string result = Encoder::toHex(inputStr);
return env->NewStringUTF(result.c_str());
}
extern "C" JNIEXPORT jstring JNICALL
Java_com_example_secureencoder_NativeEncoder_fromHex(
JNIEnv* env,
jobject /* this */,
jstring input
) {
std::string inputStr = jstringToString(env, input);
std::string result = Encoder::fromHex(inputStr);
return env->NewStringUTF(result.c_str());
}
Step 3: Configure CMakeLists.txt
# CMakeLists.txt
cmake_minimum_required(VERSION 3.22.1)
project("secure-encoder")
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_library(
secure-encoder
SHARED
jni_bridge.cpp
encoder.cpp
base64.cpp
)
find_library(
log-lib
log
)
target_link_libraries(
secure-encoder
${log-lib}
)
Step 4: Create the Kotlin Interface
// NativeEncoder.kt
package com.example.secureencoder
object NativeEncoder {
init {
System.loadLibrary("secure-encoder")
}
// Base64
external fun base64Encode(input: String): String
external fun base64Decode(input: String): String
// ROT13
external fun rot13(input: String): String
// Caesar cipher
external fun caesarEncrypt(input: String, shift: Int): String
external fun caesarDecrypt(input: String, shift: Int): String
// XOR encryption (returns hex encoded string)
external fun xorEncrypt(input: String, key: String): String
external fun xorDecrypt(input: String, key: String): String
// URL encoding
external fun urlEncode(input: String): String
external fun urlDecode(input: String): String
// Hex encoding
external fun toHex(input: String): String
external fun fromHex(input: String): String
}
enum class EncodingType {
BASE64,
ROT13,
CAESAR,
XOR,
URL,
HEX
}
Step 5: Create the Compose UI
// ui/EncoderScreen.kt
package com.example.secureencoder.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ContentCopy
import androidx.compose.material.icons.filled.SwapVert
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.dp
import com.example.secureencoder.EncodingType
import com.example.secureencoder.NativeEncoder
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun EncoderScreen() {
var inputText by remember { mutableStateOf("") }
var outputText by remember { mutableStateOf("") }
var selectedEncoding by remember { mutableStateOf(EncodingType.BASE64) }
var caesarShift by remember { mutableStateOf(3) }
var xorKey by remember { mutableStateOf("secret") }
var isEncoding by remember { mutableStateOf(true) }
val clipboardManager = LocalClipboardManager.current
fun performOperation() {
outputText = try {
when (selectedEncoding) {
EncodingType.BASE64 -> {
if (isEncoding) NativeEncoder.base64Encode(inputText)
else NativeEncoder.base64Decode(inputText)
}
EncodingType.ROT13 -> {
NativeEncoder.rot13(inputText)
}
EncodingType.CAESAR -> {
if (isEncoding) NativeEncoder.caesarEncrypt(inputText, caesarShift)
else NativeEncoder.caesarDecrypt(inputText, caesarShift)
}
EncodingType.XOR -> {
if (isEncoding) NativeEncoder.xorEncrypt(inputText, xorKey)
else NativeEncoder.xorDecrypt(inputText, xorKey)
}
EncodingType.URL -> {
if (isEncoding) NativeEncoder.urlEncode(inputText)
else NativeEncoder.urlDecode(inputText)
}
EncodingType.HEX -> {
if (isEncoding) NativeEncoder.toHex(inputText)
else NativeEncoder.fromHex(inputText)
}
}
} catch (e: Exception) {
"Error: ${e.message}"
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("Secure Text Encoder") },
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Encoding type selection
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Encoding Type",
style = MaterialTheme.typography.titleMedium
)
Spacer(modifier = Modifier.height(8.dp))
EncodingType.values().forEach { encoding ->
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
RadioButton(
selected = selectedEncoding == encoding,
onClick = { selectedEncoding = encoding }
)
Text(
text = encoding.name,
modifier = Modifier.padding(start = 8.dp)
)
}
}
}
}
// Additional options based on encoding type
when (selectedEncoding) {
EncodingType.CAESAR -> {
Card(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text("Caesar Shift: $caesarShift")
Slider(
value = caesarShift.toFloat(),
onValueChange = { caesarShift = it.toInt() },
valueRange = 1f..25f,
steps = 23
)
}
}
}
EncodingType.XOR -> {
OutlinedTextField(
value = xorKey,
onValueChange = { xorKey = it },
label = { Text("XOR Key") },
modifier = Modifier.fillMaxWidth()
)
}
else -> {}
}
// Encode/Decode toggle
if (selectedEncoding != EncodingType.ROT13) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text("Decode")
Switch(
checked = isEncoding,
onCheckedChange = { isEncoding = it },
modifier = Modifier.padding(horizontal = 8.dp)
)
Text("Encode")
}
}
// Input field
OutlinedTextField(
value = inputText,
onValueChange = { inputText = it },
label = { Text("Input Text") },
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
maxLines = 5
)
// Process button
Button(
onClick = { performOperation() },
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.SwapVert,
contentDescription = null,
modifier = Modifier.padding(end = 8.dp)
)
Text(if (isEncoding || selectedEncoding == EncodingType.ROT13) "Encode" else "Decode")
}
// Output field
OutlinedTextField(
value = outputText,
onValueChange = {},
label = { Text("Output") },
modifier = Modifier
.fillMaxWidth()
.height(120.dp),
readOnly = true,
maxLines = 5,
trailingIcon = {
IconButton(
onClick = {
clipboardManager.setText(AnnotatedString(outputText))
}
) {
Icon(
imageVector = Icons.Default.ContentCopy,
contentDescription = "Copy to clipboard"
)
}
}
)
// Quick swap button
OutlinedButton(
onClick = {
inputText = outputText
outputText = ""
},
modifier = Modifier.fillMaxWidth()
) {
Text("Use Output as Input")
}
}
}
}
Step 6: MainActivity
// MainActivity.kt
package com.example.secureencoder
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import com.example.secureencoder.ui.EncoderScreen
import com.example.secureencoder.ui.theme.SecureEncoderTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
SecureEncoderTheme {
Surface(color = MaterialTheme.colorScheme.background) {
EncoderScreen()
}
}
}
}
}
Summary
In this chapter, you've mastered the essential skill of data exchange between Kotlin and native code:
- Primitive Type Mapping - How Kotlin types map to JNI types and their size guarantees
- Passing and Returning Primitives - Seamless data flow in both directions
- String Handling - The complexities of jstring, UTF-8 encoding, and memory management
- Memory Safety - Preventing leaks with proper release patterns and RAII
- Common Pitfalls - Type conversion issues, null handling, and encoding problems
- Practical Application - Built a complete encoding library with multiple algorithms
Key Takeaways
- JNI types have guaranteed sizes across all platforms
- Always check for null before using strings
- Every
GetStringUTFCharsmust have a matchingReleaseStringUTFChars - Use RAII patterns for automatic resource cleanup
- Be aware of encoding differences between Kotlin strings and C strings
- XOR encrypted data should be hex-encoded for safe string transmission
What's Next
In Chapter 3, we'll tackle arrays, objects, and memory management in depth. You'll learn how to pass image data to native code and build a Native Image Processor with real-time filters.
Exercises
Vigenère Cipher: Implement the Vigenère cipher in native code, which uses a keyword to shift each letter differently.
String Statistics: Create native functions to analyze text: count words, sentences, vowels, and consonants.
Custom Encoding: Implement a simple substitution cipher where each letter maps to another based on a provided alphabet key.
Performance Comparison: Create a benchmark comparing Base64 encoding in native code vs. Kotlin's built-in Base64 encoder. Measure the difference with various input sizes.
Unicode Handling: Extend the Caesar cipher to properly handle Unicode characters beyond ASCII.