← Back to books

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

  1. Every GetStringUTFChars must have a matching ReleaseStringUTFChars
  2. Every GetStringCritical must have a matching ReleaseStringCritical
  3. NewStringUTF creates a new Java object — don't free it manually
  4. 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:

  1. Primitive Type Mapping - How Kotlin types map to JNI types and their size guarantees
  2. Passing and Returning Primitives - Seamless data flow in both directions
  3. String Handling - The complexities of jstring, UTF-8 encoding, and memory management
  4. Memory Safety - Preventing leaks with proper release patterns and RAII
  5. Common Pitfalls - Type conversion issues, null handling, and encoding problems
  6. 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 GetStringUTFChars must have a matching ReleaseStringUTFChars
  • 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

  1. Vigenère Cipher: Implement the Vigenère cipher in native code, which uses a keyword to shift each letter differently.

  2. String Statistics: Create native functions to analyze text: count words, sentences, vowels, and consonants.

  3. Custom Encoding: Implement a simple substitution cipher where each letter maps to another based on a provided alphabet key.

  4. 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.

  5. Unicode Handling: Extend the Caesar cipher to properly handle Unicode characters beyond ASCII.


Additional Resources