xamarin android是使用的C#语言,而C#中通过P/Invoke调用一个使用C++编写的.so库方法和java android中使用JNI去调用.so库方法是不一样的,写对应的.so库也不能去使用JNI的方式去写


现在针对不同类型的返回值来进行cpp文件的书写,并且需要在C#端进行适当的声明和内存管理

1.返回值为char* 类型的函数

这类函数通常用于C#中返回字符串数据

C++代码

假设有一个返回加密结果的函数:

其中的AES是一个加密算法

extern "C" {
    char* EncryptFromJNI(const char* raw, const char* key) {
        if (raw == nullptr || key == nullptr) return nullptr;

        AES aes_en((unsigned char*)key);
        char encrypt[1024] = {0};
        aes_en.Cipher((char*)raw, encrypt); // 假设Cipher可以处理直接的char*输入输出
        
        LOGI("Encrypt result: %s", encrypt);
        return strdup(encrypt); // 返回动态分配的字符串,需要在C#端释放
    }
}

C#代码

在C#中声明P/Invoke并调用该函数:

其中as_jni_project是对应上面cpp代码生成的.so库,去除了前缀lib和后缀.so

public class JNI_Wrapper
{
    static JNI_Wrapper()
    {
        Java.Lang.JavaSystem.LoadLibrary("as_jni_project");
    }
    public const string DLLName = "as_jni_project";
    [DllImport(DLLName, EntryPoint = "EncryptFromJNI")]
    public static extern IntPtr EncryptFromJNI(string raw,string key);
    [DllImport(DLLName, EntryPoint = "DecryptFromJNI")]
    public static extern IntPtr DecryptFromJNI(string res, string key);
    public static string Encrypt(string raw,string key)
    {
        IntPtr ptr = EncryptFromJNI(raw, key);
        if (ptr == IntPtr.Zero) return null; // 检查空指针
        try
        {
            string result = Marshal.PtrToStringAnsi(ptr); // 将指针转换为C#字符串
            return result;
        }
        finally
        {
            Marshal.FreeCoTaskMem(ptr); // 或者使用Marshal.FreeHGlobal,取决于C++侧如何分配内存
        }
    }
}

PS:如果在C++中使用malloc分配内存,则应使用Marshal.FreeHGlobal来释放内存;如果使用的是strdup或类似的方式,通常也需要使用Marshal.FreeHGlobal。


2.返回值为数组类型的函数

对于返回数组可以通过指针返回数组的首地址。

C++代码

假设有一个返回整数数组的函数:

extern "C" {
    int* GetIntArray(int size) {
        int* arr = (int*)malloc(size * sizeof(int));
        for(int i = 0; i < size; ++i) {
            arr[i] = i;
        }
        return arr; // 注意:调用者需负责释放这块内存
    }
}

C#代码

在C#中声明P/Invoke并调用该函数:

public class JNI_Wrapper
{
    [DllImport("as_jni_project", CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr GetIntArray(int size);

    public static int[] GetArray(int size)
    {
        IntPtr ptr = GetIntArray(size);
        if (ptr == IntPtr.Zero) return null;

        try
        {
            int[] array = new int[size];
            Marshal.Copy(ptr, array, 0, size); // 将非托管内存复制到托管数组
            return array;
        }
        finally
        {
            Marshal.FreeHGlobal(ptr); // 释放由C++分配的内存
        }
    }
}

// 使用示例
class Program
{
    static void Main()
    {
        int[] array = JNI_Wrapper.GetArray(5);
        foreach(var item in array)
        {
            Console.WriteLine(item);
        }
    }
}

假设返回byte[]类型:

C++代码

unsigned关键字在C/C++中用于指定整型变量或类型为无符号(即只能表示非负数值)

1. 数据范围:char类型在C/C++中可能是有符号的也可能是无符号的,这取决于编译器和平台。当您需要确保字符值的范围是0到255(而不是-128到127)时,应该使用unsigned char。这是因为字节(byte)通常用来表示图像、音频等二进制数据,这些数据不应被解释为负数。

2. 特定用途:在这个函数中,您正在生成一个字节数组(byte array),每个元素代表一个字节(8位)。由于字节的取值范围是从0到255,使用unsigned char可以明确地表达这一点,并避免潜在的误解或错误。

extern "C" {
    unsigned char* GetByteArray(int size) {
        unsigned char* arr = (unsigned char*)malloc(size * sizeof(unsigned char));
        for(int i = 0; i < size; ++i) {
            arr[i] = i % 256; // 填充一些示例数据
        }
        return arr; // 注意:调用者需负责释放这块内存
    }
}

C#代码:

在C#中声明P/Invoke并调用该函数:

public class JNI_Wrapper
{
    [DllImport("as_jni_project", CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr GetByteArray(int size);

    public static byte[] GetByteArray(int size)
    {
        IntPtr ptr = GetByteArray(size);
        if (ptr == IntPtr.Zero) return null;

        try
        {
            byte[] array = new byte[size];
            Marshal.Copy(ptr, array, 0, size); // 将非托管内存复制到托管数组
            return array;
        }
        finally
        {
            Marshal.FreeHGlobal(ptr); // 释放由C++分配的内存
        }
    }
}

// 使用示例
class Program
{
    static void Main()
    {
        byte[] byteArray = JNI_Wrapper.GetByteArray(10);
        foreach(var item in byteArray)
        {
            Console.WriteLine(item);
        }
    }
}

假设返回string[]类型:

对于字符串数组的情况稍微复杂一些,因为直接返回一个指针数组(即char**)可能不是最简单的方法。一种常见的方法是使用结构体或通过传递缓冲区的方式来实现

C++代码:

typedef struct {
    int count;
    char* strings[10]; // 假设最多有10个字符串
} StringArray;

extern "C" {
    StringArray GetStringArray() {
        StringArray sa = {0};
        const char* sampleStrings[] = {"Hello", "World", "From", "C++", "JNI"};
        sa.count = 5; // 设置实际的字符串数量
        for(int i = 0; i < sa.count; ++i) {
            sa.strings[i] = strdup(sampleStrings[i]); // 动态分配每个字符串
        }
        return sa;
    }
}

C#代码:

在C#中声明P/Invoke并调用该函数:

使用了[StructLayout(LayoutKind.Sequential)]来确保C#结构体成员的内存布局顺序与非托管代码中的结构体保持一致。此外,还使用了[MarshalAs]特性指定了数组的大小,这对于正确映射到非托管代码中的固定大小数组非常重要

[StructLayout(LayoutKind.Sequential)]
public struct StringArray
{
    public int Count;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 10)]
    public IntPtr[] Strings;
}

public class JNI_Wrapper
{
    [DllImport("as_jni_project", CallingConvention = CallingConvention.Cdecl)]
    private static extern StringArray GetStringArray();

    public static string[] GetStringArray()
    {
        StringArray sa = GetStringArray();
        string[] result = new string[sa.Count];

        for(int i = 0; i < sa.Count; ++i)
        {
            result[i] = Marshal.PtrToStringAnsi(sa.Strings[i]);
            Marshal.FreeCoTaskMem(sa.Strings[i]); // 或者使用Marshal.FreeHGlobal,取决于C++侧如何分配内存
        }

        return result;
    }
}

// 使用示例
class Program
{
    static void Main()
    {
        string[] stringArray = JNI_Wrapper.GetStringArray();
        foreach(var item in stringArray)
        {
            Console.WriteLine(item);
        }
    }
}

3.返回结构体类型的函数

需要返回复杂的结构数据时,可以使用结构体

C++代码

定义并实现一个返回结构体的函数:

typedef struct {
    int id;
    char name[50];
} Person;

extern "C" {
    Person CreatePerson(int id, const char* name) {
        Person person = {id, ""};
        strncpy(person.name, name, 49); // 确保不会超出边界
        person.name[49] = '\0'; // 确保字符串以null终止
        return person;
    }
}

C#代码

在C#中声明P/Invoke并调用该函数:

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 50)]:这个属性用于指定字符串字段Name应被视为长度为50的固定大小字符数组,并且该字符串将以ANSI编码方式进行封送处理。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct Person
{
    public int Id;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 50)]
    public string Name;
}

public class JNI_Wrapper
{
    [DllImport("as_jni_project", CallingConvention = CallingConvention.Cdecl)]
    private static extern Person CreatePerson(int id, string name);

    public static Person CreatePersonWrapper(int id, string name)
    {
        return CreatePerson(id, name);
    }
}

// 使用示例
class Program
{
    static void Main()
    {
        var person = JNI_Wrapper.CreatePersonWrapper(1, "John Doe");
        Console.WriteLine($"ID: {person.Id}, Name: {person.Name}");
    }
}

更多推荐