← Volver al listado de tecnologías

Extensiones Nativas y Integración con C++

Por: Artiko
defoldc++extensiones-nativasoptimizacionjniobjective-c

Extensiones Nativas y Integración con C++

Las extensiones nativas permiten integrar código C++ de alto rendimiento, acceder a APIs específicas de plataforma y utilizar bibliotecas externas. En esta lección aprenderás a crear y optimizar extensiones nativas para Defold.

🔧 Fundamentos de Extensiones Nativas

Estructura de Proyecto

/myextension/
├── ext.manifest
├── src/
│   ├── extension.cpp
│   └── extension.h
├── lib/
│   ├── android/
│   │   └── libmylib.a
│   ├── ios/
│   │   └── libmylib.a
│   └── common/
│       └── mylib.h
├── manifests/
│   ├── android/
│   │   └── AndroidManifest.xml
│   └── ios/
│       └── Info.plist
└── include/
    └── myextension/
        └── myextension.h

ext.manifest Configuration

# ext.manifest
name: "MyExtension"

platforms:
    android:
        context:
            aaptExtraPackages: ["com.example.myextension"]

        libs: ["android/libmylib.a"]

        linkFlags: ["-llog", "-landroid"]

        jetifier: true

        proguard: "proguard-project.txt"

    ios:
        libs: ["ios/libmylib.a"]

        frameworks: ["CoreMotion", "CoreLocation", "StoreKit"]

        linkFlags: ["-ObjC"]

    osx:
        libs: ["osx/libmylib.a"]

        frameworks: ["CoreServices"]

    win32:
        libs: ["win32/mylib.lib"]

    linux:
        libs: ["linux/libmylib.a"]

    html5:
        libs: ["js-web/library_myextension.js"]

💻 Extensión Básica en C++

Header File (extension.h)

// extension.h
#pragma once

#include <dmsdk/sdk.h>

#if defined(DM_PLATFORM_ANDROID)
#include <jni.h>
#include <android/log.h>
#endif

#if defined(DM_PLATFORM_IOS) || defined(DM_PLATFORM_OSX)
#include <Foundation/Foundation.h>
#endif

namespace myextension {

// Estructura para datos de configuración
struct ExtensionConfig {
    bool enabled;
    float update_frequency;
    int max_items;
};

// Funciones principales
int Initialize();
int Finalize();
void Update(float dt);

// Funciones específicas de plataforma
#if defined(DM_PLATFORM_ANDROID)
void InitializeAndroid(JNIEnv* env, jobject activity);
#endif

#if defined(DM_PLATFORM_IOS)
void InitializeIOS();
#endif

// Funciones exportadas a Lua
int LuaGetDeviceInfo(lua_State* L);
int LuaStartService(lua_State* L);
int LuaStopService(lua_State* L);
int LuaGetSensorData(lua_State* L);

} // namespace myextension

Implementation (extension.cpp)

// extension.cpp
#include "extension.h"
#include <cmath>
#include <vector>
#include <string>

namespace myextension {

static ExtensionConfig g_config = {true, 60.0f, 100};
static std::vector<float> g_sensor_data;
static bool g_service_running = false;

// === Platform-specific implementations ===

#if defined(DM_PLATFORM_ANDROID)

static JNIEnv* g_env = nullptr;
static jobject g_activity = nullptr;

void InitializeAndroid(JNIEnv* env, jobject activity) {
    g_env = env;
    g_activity = activity;

    __android_log_print(ANDROID_LOG_INFO, "MyExtension", "Android initialization complete");
}

// JNI function to call Java methods
jstring CallJavaMethod(const char* method_name, const char* signature) {
    if (!g_env || !g_activity) return nullptr;

    jclass activity_class = g_env->GetObjectClass(g_activity);
    jmethodID method_id = g_env->GetMethodID(activity_class, method_name, signature);

    if (method_id) {
        return (jstring)g_env->CallObjectMethod(g_activity, method_id);
    }

    g_env->DeleteLocalRef(activity_class);
    return nullptr;
}

#elif defined(DM_PLATFORM_IOS)

void InitializeIOS() {
    NSLog(@"MyExtension: iOS initialization complete");
}

NSString* GetIOSDeviceInfo() {
    UIDevice* device = [UIDevice currentDevice];
    return [NSString stringWithFormat:@"%@ %@ %@",
           device.model, device.systemName, device.systemVersion];
}

#endif

// === Cross-platform implementation ===

int Initialize() {
    dmLogInfo("MyExtension: Initializing...");

    // Configuración inicial
    g_config.enabled = true;
    g_sensor_data.reserve(1000);

#if defined(DM_PLATFORM_ANDROID)
    // La inicialización Android se hace en app_initialize
#elif defined(DM_PLATFORM_IOS)
    InitializeIOS();
#endif

    dmLogInfo("MyExtension: Initialization complete");
    return 0;
}

int Finalize() {
    dmLogInfo("MyExtension: Finalizing...");

    g_sensor_data.clear();
    g_service_running = false;

    dmLogInfo("MyExtension: Finalization complete");
    return 0;
}

void Update(float dt) {
    if (!g_config.enabled || !g_service_running) return;

    // Simular datos de sensor
    static float time_accumulator = 0.0f;
    time_accumulator += dt;

    if (time_accumulator >= 1.0f / g_config.update_frequency) {
        float sensor_value = std::sin(time_accumulator) * 100.0f;
        g_sensor_data.push_back(sensor_value);

        // Mantener solo los últimos 100 valores
        if (g_sensor_data.size() > g_config.max_items) {
            g_sensor_data.erase(g_sensor_data.begin());
        }

        time_accumulator = 0.0f;
    }
}

// === Lua bindings ===

int LuaGetDeviceInfo(lua_State* L) {
    std::string device_info = "Unknown";

#if defined(DM_PLATFORM_ANDROID)
    jstring java_result = CallJavaMethod("getDeviceInfo", "()Ljava/lang/String;");
    if (java_result) {
        const char* chars = g_env->GetStringUTFChars(java_result, nullptr);
        device_info = std::string(chars);
        g_env->ReleaseStringUTFChars(java_result, chars);
        g_env->DeleteLocalRef(java_result);
    }
#elif defined(DM_PLATFORM_IOS)
    NSString* ios_info = GetIOSDeviceInfo();
    device_info = std::string([ios_info UTF8String]);
#elif defined(DM_PLATFORM_WIN32)
    device_info = "Windows PC";
#elif defined(DM_PLATFORM_LINUX)
    device_info = "Linux PC";
#elif defined(DM_PLATFORM_OSX)
    device_info = "macOS";
#endif

    lua_pushstring(L, device_info.c_str());
    return 1;
}

int LuaStartService(lua_State* L) {
    if (lua_gettop(L) >= 1) {
        g_config.update_frequency = luaL_checknumber(L, 1);
    }

    g_service_running = true;
    lua_pushboolean(L, 1);

    dmLogInfo("MyExtension: Service started with frequency %.2f", g_config.update_frequency);
    return 1;
}

int LuaStopService(lua_State* L) {
    g_service_running = false;
    g_sensor_data.clear();

    lua_pushboolean(L, 1);

    dmLogInfo("MyExtension: Service stopped");
    return 1;
}

int LuaGetSensorData(lua_State* L) {
    lua_newtable(L);

    for (size_t i = 0; i < g_sensor_data.size(); ++i) {
        lua_pushnumber(L, g_sensor_data[i]);
        lua_rawseti(L, -2, i + 1);
    }

    return 1;
}

// === Extension lifecycle ===

static const luaL_Reg lua_functions[] = {
    {"get_device_info", LuaGetDeviceInfo},
    {"start_service", LuaStartService},
    {"stop_service", LuaStopService},
    {"get_sensor_data", LuaGetSensorData},
    {nullptr, nullptr}
};

static dmExtension::Result AppInitialize(dmExtension::AppParams* params) {
    dmLogInfo("MyExtension: App Initialize");
    return dmExtension::RESULT_OK;
}

static dmExtension::Result Initialize(dmExtension::Params* params) {
    dmLogInfo("MyExtension: Extension Initialize");

    // Registrar funciones Lua
    luaL_register(params->m_L, "myextension", lua_functions);
    lua_pop(params->m_L, 1);

    return Initialize() == 0 ? dmExtension::RESULT_OK : dmExtension::RESULT_INIT_ERROR;
}

static dmExtension::Result AppFinalize(dmExtension::AppParams* params) {
    dmLogInfo("MyExtension: App Finalize");
    return dmExtension::RESULT_OK;
}

static dmExtension::Result Finalize(dmExtension::Params* params) {
    dmLogInfo("MyExtension: Extension Finalize");
    return Finalize() == 0 ? dmExtension::RESULT_OK : dmExtension::RESULT_UNKNOWN_ERROR;
}

static dmExtension::Result OnUpdate(dmExtension::Params* params) {
    Update(params->m_DT);
    return dmExtension::RESULT_OK;
}

} // namespace myextension

// Registro de la extensión
DM_DECLARE_EXTENSION(MyExtension, "MyExtension", myextension::AppInitialize, myextension::AppFinalize, myextension::Initialize, 0, myextension::OnUpdate, myextension::Finalize);

📱 Integración Android (JNI)

Java Bridge Class

// MyExtensionActivity.java
package com.example.myextension;

import android.app.Activity;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.os.Build;
import android.util.Log;

public class MyExtensionActivity extends Activity implements SensorEventListener {
    private static final String TAG = "MyExtension";

    private SensorManager sensorManager;
    private Sensor accelerometer;
    private float[] lastAccelerometer = new float[3];
    private boolean isAccelerometerActive = false;

    public void initialize() {
        Log.i(TAG, "Java bridge initializing...");

        sensorManager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
        accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);

        if (accelerometer != null) {
            sensorManager.registerListener(this, accelerometer, SensorManager.SENSOR_DELAY_GAME);
            isAccelerometerActive = true;
            Log.i(TAG, "Accelerometer activated");
        }
    }

    public String getDeviceInfo() {
        return String.format("%s %s (API %d)",
                           Build.MANUFACTURER,
                           Build.MODEL,
                           Build.VERSION.SDK_INT);
    }

    public float[] getAccelerometerData() {
        if (isAccelerometerActive) {
            return lastAccelerometer.clone();
        }
        return new float[]{0, 0, 0};
    }

    public boolean startVibration(int duration) {
        try {
            android.os.Vibrator vibrator = (android.os.Vibrator) getSystemService(Context.VIBRATOR_SERVICE);
            if (vibrator != null && vibrator.hasVibrator()) {
                vibrator.vibrate(duration);
                return true;
            }
        } catch (Exception e) {
            Log.e(TAG, "Vibration failed: " + e.getMessage());
        }
        return false;
    }

    @Override
    public void onSensorChanged(SensorEvent event) {
        if (event.sensor.getType() == Sensor.TYPE_ACCELEROMETER) {
            System.arraycopy(event.values, 0, lastAccelerometer, 0, event.values.length);
        }
    }

    @Override
    public void onAccuracyChanged(Sensor sensor, int accuracy) {
        // No necesario para este ejemplo
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (sensorManager != null) {
            sensorManager.unregisterListener(this);
        }
    }
}

JNI Bridge Functions

// android_bridge.cpp
#if defined(DM_PLATFORM_ANDROID)

#include <jni.h>
#include <android/log.h>

extern "C" {

// Funciones llamadas desde Java
JNIEXPORT void JNICALL
Java_com_example_myextension_MyExtensionActivity_nativeAccelerometerUpdate(
    JNIEnv *env, jobject thiz, jfloatArray values) {

    jfloat* array_elements = env->GetFloatArrayElements(values, nullptr);
    jsize array_length = env->GetArrayLength(values);

    if (array_length >= 3) {
        // Procesar datos del acelerómetro
        float x = array_elements[0];
        float y = array_elements[1];
        float z = array_elements[2];

        // Enviar datos al sistema de extensión
        myextension::UpdateAccelerometerData(x, y, z);
    }

    env->ReleaseFloatArrayElements(values, array_elements, JNI_ABORT);
}

JNIEXPORT jstring JNICALL
Java_com_example_myextension_MyExtensionActivity_nativeGetVersion(
    JNIEnv *env, jobject thiz) {

    return env->NewStringUTF("MyExtension v1.0.0");
}

} // extern "C"

namespace myextension {

// Funciones helper para JNI
jclass FindClass(JNIEnv* env, const char* class_name) {
    jclass clazz = env->FindClass(class_name);
    if (!clazz) {
        __android_log_print(ANDROID_LOG_ERROR, "MyExtension",
                          "Failed to find class: %s", class_name);
    }
    return clazz;
}

jmethodID GetMethodID(JNIEnv* env, jclass clazz, const char* method_name, const char* signature) {
    jmethodID method = env->GetMethodID(clazz, method_name, signature);
    if (!method) {
        __android_log_print(ANDROID_LOG_ERROR, "MyExtension",
                          "Failed to find method: %s", method_name);
    }
    return method;
}

// Llamar método Java desde C++
bool CallJavaVibrate(int duration) {
    if (!g_env || !g_activity) return false;

    jclass activity_class = g_env->GetObjectClass(g_activity);
    jmethodID vibrate_method = GetMethodID(g_env, activity_class, "startVibration", "(I)Z");

    if (vibrate_method) {
        jboolean result = g_env->CallBooleanMethod(g_activity, vibrate_method, duration);
        g_env->DeleteLocalRef(activity_class);
        return result;
    }

    g_env->DeleteLocalRef(activity_class);
    return false;
}

} // namespace myextension

#endif // DM_PLATFORM_ANDROID

🍎 Integración iOS (Objective-C)

Objective-C Bridge

// ios_bridge.mm
#if defined(DM_PLATFORM_IOS)

#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <CoreMotion/CoreMotion.h>
#import <AVFoundation/AVFoundation.h>

@interface MyExtensionBridge : NSObject
@property (strong, nonatomic) CMMotionManager *motionManager;
@property (strong, nonatomic) AVAudioPlayer *audioPlayer;
@end

@implementation MyExtensionBridge

- (instancetype)init {
    self = [super init];
    if (self) {
        self.motionManager = [[CMMotionManager alloc] init];
        [self setupMotionUpdates];
    }
    return self;
}

- (void)setupMotionUpdates {
    if (self.motionManager.accelerometerAvailable) {
        self.motionManager.accelerometerUpdateInterval = 1.0 / 60.0; // 60 Hz

        [self.motionManager startAccelerometerUpdatesToQueue:[NSOperationQueue mainQueue]
                                                 withHandler:^(CMAccelerometerData *data, NSError *error) {
            if (!error) {
                myextension::UpdateAccelerometerData(data.acceleration.x,
                                                   data.acceleration.y,
                                                   data.acceleration.z);
            }
        }];
    }
}

- (NSString*)getDeviceInfo {
    UIDevice *device = [UIDevice currentDevice];
    return [NSString stringWithFormat:@"%@ %@ %@",
           device.model, device.systemName, device.systemVersion];
}

- (BOOL)playSound:(NSString*)filename {
    NSString *soundPath = [[NSBundle mainBundle] pathForResource:filename ofType:nil];
    if (!soundPath) return NO;

    NSURL *soundURL = [NSURL fileURLWithPath:soundPath];
    NSError *error;

    self.audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:soundURL error:&error];
    if (error) {
        NSLog(@"Audio player error: %@", error.localizedDescription);
        return NO;
    }

    [self.audioPlayer play];
    return YES;
}

- (void)vibrate {
    AudioServicesPlaySystemSound(kSystemSoundID_Vibrate);
}

- (void)dealloc {
    [self.motionManager stopAccelerometerUpdates];
}

@end

// C++ wrapper functions
namespace myextension {

static MyExtensionBridge* g_bridge = nullptr;

void InitializeIOSBridge() {
    g_bridge = [[MyExtensionBridge alloc] init];
}

void FinalizeIOSBridge() {
    g_bridge = nullptr;
}

std::string GetIOSDeviceInfo() {
    if (g_bridge) {
        NSString* info = [g_bridge getDeviceInfo];
        return std::string([info UTF8String]);
    }
    return "iOS Device";
}

bool PlayIOSSound(const char* filename) {
    if (g_bridge) {
        NSString* file = [NSString stringWithUTF8String:filename];
        return [g_bridge playSound:file];
    }
    return false;
}

void TriggerIOSVibration() {
    if (g_bridge) {
        [g_bridge vibrate];
    }
}

} // namespace myextension

#endif // DM_PLATFORM_IOS

⚡ Optimización de Rendimiento

Threading y Async Operations

// threaded_extension.cpp
#include <thread>
#include <mutex>
#include <queue>
#include <condition_variable>
#include <atomic>

namespace myextension {

// Estructura para tareas asíncronas
struct AsyncTask {
    std::function<void()> function;
    std::function<void()> callback;
    int priority;
};

class ThreadPool {
private:
    std::vector<std::thread> workers;
    std::queue<AsyncTask> tasks;
    std::mutex queue_mutex;
    std::condition_variable condition;
    std::atomic<bool> stop{false};

public:
    ThreadPool(size_t num_threads = std::thread::hardware_concurrency()) {
        for (size_t i = 0; i < num_threads; ++i) {
            workers.emplace_back([this] {
                while (!stop) {
                    AsyncTask task;

                    {
                        std::unique_lock<std::mutex> lock(queue_mutex);
                        condition.wait(lock, [this] { return stop || !tasks.empty(); });

                        if (stop && tasks.empty()) return;

                        task = std::move(tasks.front());
                        tasks.pop();
                    }

                    // Ejecutar tarea
                    task.function();

                    // Ejecutar callback en main thread si existe
                    if (task.callback) {
                        // Aquí normalmente añadirías el callback a una cola para main thread
                        main_thread_callbacks.push(task.callback);
                    }
                }
            });
        }
    }

    ~ThreadPool() {
        stop = true;
        condition.notify_all();

        for (std::thread& worker : workers) {
            if (worker.joinable()) {
                worker.join();
            }
        }
    }

    void EnqueueTask(AsyncTask task) {
        {
            std::unique_lock<std::mutex> lock(queue_mutex);
            tasks.push(std::move(task));
        }
        condition.notify_one();
    }

private:
    std::queue<std::function<void()>> main_thread_callbacks;
};

static std::unique_ptr<ThreadPool> g_thread_pool;

// Función para procesar callbacks en main thread
void ProcessMainThreadCallbacks() {
    // Esta función debe llamarse desde el main thread regularmente
    while (!main_thread_callbacks.empty()) {
        auto callback = main_thread_callbacks.front();
        main_thread_callbacks.pop();
        callback();
    }
}

// Ejemplo de tarea pesada asíncrona
void PerformHeavyCalculation(const std::vector<float>& data,
                           std::function<void(float)> on_complete) {

    AsyncTask task;
    task.function = [data]() {
        // Simulación de cálculo pesado
        float result = 0.0f;
        for (float value : data) {
            result += std::sqrt(value * value + 1.0f);
            std::this_thread::sleep_for(std::chrono::microseconds(1));
        }
        return result;
    };

    task.callback = [on_complete, result = 0.0f]() mutable {
        on_complete(result);
    };

    g_thread_pool->EnqueueTask(std::move(task));
}

} // namespace myextension

Memory Management

// memory_manager.cpp
namespace myextension {

// Pool de memoria personalizado
template<typename T, size_t PoolSize>
class ObjectPool {
private:
    alignas(T) char storage[PoolSize * sizeof(T)];
    std::bitset<PoolSize> used;
    std::mutex pool_mutex;

public:
    template<typename... Args>
    T* Acquire(Args&&... args) {
        std::lock_guard<std::mutex> lock(pool_mutex);

        for (size_t i = 0; i < PoolSize; ++i) {
            if (!used[i]) {
                used[i] = true;
                T* obj = reinterpret_cast<T*>(&storage[i * sizeof(T)]);
                new(obj) T(std::forward<Args>(args)...);
                return obj;
            }
        }

        // Pool lleno, usar memoria dinámica como fallback
        dmLogWarning("ObjectPool full, falling back to dynamic allocation");
        return new T(std::forward<Args>(args)...);
    }

    void Release(T* obj) {
        std::lock_guard<std::mutex> lock(pool_mutex);

        // Verificar si el objeto está en el pool
        char* obj_ptr = reinterpret_cast<char*>(obj);
        if (obj_ptr >= storage && obj_ptr < storage + sizeof(storage)) {
            size_t index = (obj_ptr - storage) / sizeof(T);
            if (index < PoolSize && used[index]) {
                obj->~T();
                used[index] = false;
                return;
            }
        }

        // Objeto no está en el pool, eliminar dinámicamente
        delete obj;
    }
};

// Ejemplo de uso
static ObjectPool<SensorData, 1000> g_sensor_pool;

SensorData* CreateSensorData(float x, float y, float z) {
    return g_sensor_pool.Acquire(x, y, z);
}

void DestroySensorData(SensorData* data) {
    g_sensor_pool.Release(data);
}

} // namespace myextension

🔌 Integración de Bibliotecas Externas

Ejemplo: OpenCV Integration

// opencv_extension.cpp
#ifdef USE_OPENCV
#include <opencv2/opencv.hpp>
#include <opencv2/imgproc.hpp>

namespace myextension {

class ImageProcessor {
private:
    cv::Mat current_frame;
    cv::CascadeClassifier face_cascade;

public:
    bool Initialize() {
        // Cargar clasificador de caras
        std::string cascade_path = "/assets/haarcascade_frontalface_alt.xml";
        if (!face_cascade.load(cascade_path)) {
            dmLogError("Failed to load face cascade");
            return false;
        }

        dmLogInfo("OpenCV Image Processor initialized");
        return true;
    }

    std::vector<cv::Rect> DetectFaces(const uint8_t* image_data,
                                     int width, int height, int channels) {
        cv::Mat image(height, width, channels == 4 ? CV_8UC4 : CV_8UC3,
                     const_cast<uint8_t*>(image_data));

        cv::Mat gray;
        if (channels > 1) {
            cv::cvtColor(image, gray, cv::COLOR_RGB2GRAY);
        } else {
            gray = image;
        }

        std::vector<cv::Rect> faces;
        face_cascade.detectMultiScale(gray, faces, 1.1, 3, 0, cv::Size(30, 30));

        return faces;
    }

    bool ApplyFilter(uint8_t* image_data, int width, int height,
                    int channels, const std::string& filter_type) {
        cv::Mat image(height, width, channels == 4 ? CV_8UC4 : CV_8UC3, image_data);

        if (filter_type == "blur") {
            cv::GaussianBlur(image, image, cv::Size(15, 15), 0);
        } else if (filter_type == "edge") {
            cv::Mat gray, edges;
            cv::cvtColor(image, gray, cv::COLOR_RGB2GRAY);
            cv::Canny(gray, edges, 100, 200);
            cv::cvtColor(edges, image, cv::COLOR_GRAY2RGB);
        } else if (filter_type == "sepia") {
            cv::Mat kernel = (cv::Mat_<float>(4, 4) <<
                0.272, 0.534, 0.131, 0,
                0.349, 0.686, 0.168, 0,
                0.393, 0.769, 0.189, 0,
                0, 0, 0, 1);
            cv::transform(image, image, kernel);
        }

        return true;
    }
};

static std::unique_ptr<ImageProcessor> g_image_processor;

// Funciones Lua
int LuaProcessImage(lua_State* L) {
    if (!g_image_processor) {
        lua_pushboolean(L, 0);
        lua_pushstring(L, "Image processor not initialized");
        return 2;
    }

    // Obtener datos de imagen desde Lua
    size_t data_size;
    const char* image_data = luaL_checklstring(L, 1, &data_size);
    int width = luaL_checkinteger(L, 2);
    int height = luaL_checkinteger(L, 3);
    int channels = luaL_optinteger(L, 4, 3);
    const char* filter = luaL_optstring(L, 5, "none");

    // Crear copia de los datos para modificar
    std::vector<uint8_t> mutable_data(image_data, image_data + data_size);

    bool success = g_image_processor->ApplyFilter(mutable_data.data(),
                                                 width, height, channels, filter);

    if (success) {
        lua_pushlstring(L, reinterpret_cast<char*>(mutable_data.data()), data_size);
        return 1;
    } else {
        lua_pushnil(L);
        return 1;
    }
}

} // namespace myextension

#endif // USE_OPENCV

🎮 Proyecto Práctico: Sistema de Analytics Nativo

Vamos a crear una extensión completa para analytics:

1. Analytics Core

// analytics_extension.cpp
#include <json/json.h>
#include <curl/curl.h>
#include <chrono>

namespace analytics {

struct AnalyticsEvent {
    std::string event_name;
    Json::Value parameters;
    int64_t timestamp;
    std::string session_id;
};

class AnalyticsManager {
private:
    std::queue<AnalyticsEvent> event_queue;
    std::mutex queue_mutex;
    std::string api_endpoint;
    std::string api_key;
    std::string session_id;
    std::thread upload_thread;
    std::atomic<bool> running{true};

public:
    bool Initialize(const std::string& endpoint, const std::string& key) {
        api_endpoint = endpoint;
        api_key = key;
        session_id = GenerateSessionId();

        // Inicializar CURL
        curl_global_init(CURL_GLOBAL_DEFAULT);

        // Iniciar hilo de subida
        upload_thread = std::thread(&AnalyticsManager::UploadWorker, this);

        dmLogInfo("Analytics initialized with session: %s", session_id.c_str());
        return true;
    }

    void TrackEvent(const std::string& event_name, const Json::Value& parameters = Json::Value()) {
        AnalyticsEvent event;
        event.event_name = event_name;
        event.parameters = parameters;
        event.timestamp = GetCurrentTimestamp();
        event.session_id = session_id;

        {
            std::lock_guard<std::mutex> lock(queue_mutex);
            event_queue.push(event);
        }

        dmLogInfo("Event tracked: %s", event_name.c_str());
    }

    void Shutdown() {
        running = false;
        if (upload_thread.joinable()) {
            upload_thread.join();
        }
        curl_global_cleanup();
    }

private:
    std::string GenerateSessionId() {
        auto now = std::chrono::system_clock::now();
        auto timestamp = std::chrono::duration_cast<std::chrono::milliseconds>(
            now.time_since_epoch()).count();
        return "session_" + std::to_string(timestamp);
    }

    int64_t GetCurrentTimestamp() {
        auto now = std::chrono::system_clock::now();
        return std::chrono::duration_cast<std::chrono::milliseconds>(
            now.time_since_epoch()).count();
    }

    void UploadWorker() {
        while (running) {
            std::vector<AnalyticsEvent> events_to_upload;

            {
                std::lock_guard<std::mutex> lock(queue_mutex);
                while (!event_queue.empty() && events_to_upload.size() < 50) {
                    events_to_upload.push_back(event_queue.front());
                    event_queue.pop();
                }
            }

            if (!events_to_upload.empty()) {
                UploadEvents(events_to_upload);
            }

            std::this_thread::sleep_for(std::chrono::seconds(30));
        }
    }

    bool UploadEvents(const std::vector<AnalyticsEvent>& events) {
        CURL* curl = curl_easy_init();
        if (!curl) return false;

        // Construir JSON
        Json::Value batch;
        batch["session_id"] = session_id;
        batch["events"] = Json::Value(Json::arrayValue);

        for (const auto& event : events) {
            Json::Value json_event;
            json_event["name"] = event.event_name;
            json_event["timestamp"] = event.timestamp;
            json_event["parameters"] = event.parameters;
            batch["events"].append(json_event);
        }

        Json::StreamWriterBuilder builder;
        std::string json_data = Json::writeString(builder, batch);

        // Configurar CURL
        curl_easy_setopt(curl, CURLOPT_URL, api_endpoint.c_str());
        curl_easy_setopt(curl, CURLOPT_POSTFIELDS, json_data.c_str());

        struct curl_slist* headers = nullptr;
        std::string auth_header = "Authorization: Bearer " + api_key;
        headers = curl_slist_append(headers, "Content-Type: application/json");
        headers = curl_slist_append(headers, auth_header.c_str());
        curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);

        CURLcode res = curl_easy_perform(curl);

        curl_slist_free_all(headers);
        curl_easy_cleanup(curl);

        if (res == CURLE_OK) {
            dmLogInfo("Uploaded %zu analytics events", events.size());
            return true;
        } else {
            dmLogError("Failed to upload analytics: %s", curl_easy_strerror(res));
            return false;
        }
    }
};

static std::unique_ptr<AnalyticsManager> g_analytics;

// Funciones Lua
int LuaInitializeAnalytics(lua_State* L) {
    const char* endpoint = luaL_checkstring(L, 1);
    const char* api_key = luaL_checkstring(L, 2);

    g_analytics = std::make_unique<AnalyticsManager>();
    bool success = g_analytics->Initialize(endpoint, api_key);

    lua_pushboolean(L, success);
    return 1;
}

int LuaTrackEvent(lua_State* L) {
    if (!g_analytics) {
        lua_pushboolean(L, 0);
        return 1;
    }

    const char* event_name = luaL_checkstring(L, 1);

    Json::Value parameters;
    if (lua_istable(L, 2)) {
        // Convertir tabla Lua a JSON
        parameters = LuaTableToJson(L, 2);
    }

    g_analytics->TrackEvent(event_name, parameters);
    lua_pushboolean(L, 1);
    return 1;
}

} // namespace analytics

📚 Recursos y Referencias

APIs de Defold SDK

Herramientas de Desarrollo

🎯 Ejercicios Propuestos

  1. Camera Extension: Crea una extensión que acceda a la cámara del dispositivo para capturar imágenes.

  2. File System Bridge: Implementa acceso completo al sistema de archivos nativo.

  3. Social SDK Integration: Integra un SDK de redes sociales (Facebook, Twitter) usando extensiones nativas.

  4. Performance Monitor: Crea un monitor de rendimiento que reporte métricas del dispositivo.

  5. Bluetooth Extension: Implementa comunicación Bluetooth para juegos multijugador local.

Las extensiones nativas son el puente hacia el poder completo de las plataformas nativas. Dominando C++ y las APIs específicas de cada plataforma, podrás integrar cualquier funcionalidad que tu juego necesite.