You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无unsafe代码实现C#与C++数组互传及性能优化技术咨询

Unity中无unsafe调用C++ DLL处理纹理数组的最优实现

我完全懂你的处境——在Unity里处理动态生成的纹理(没法从磁盘加载),要转成int/byte/float数组,还想用C++ DLL提速,又因为兼容性要求不能碰unsafe代码,还要尽量复用数组避免内存泄漏。下面给你一套完全贴合需求的可运行方案,连内存管理的坑都帮你踩平了:

核心思路

咱们优先用传入两个数组的方案:C#侧提前创建好输入数组(从纹理转换而来)和输出数组(可复用),直接把两个数组的内存映射给C++,让C++直接修改输出数组的内容——全程不需要Marshal.Copy或者unsafe代码,性能拉满还不会有内存泄漏风险。


C++ DLL实现

首先写C的导出函数,注意要避免C的名字修饰,同时直接操作数组指针:

#include <cstring>

// 用extern "C"避免C++名字修饰,确保C#能找到函数
extern "C" {
    // 导出函数:处理输入数组,直接修改输出数组内容
    __declspec(dllexport) void ProcessTextureArray(
        const float* inputArray,  // 输入数组(只读,避免误修改)
        float* outputArray,       // 输出数组(直接修改)
        int arrayLength           // 数组长度,必须和C#侧一致
    ) {
        // 这里替换成你的目标检测逻辑,比如遍历每个元素处理
        for (int i = 0; i < arrayLength; ++i) {
            // 示例:把输入值乘2后写入输出数组
            outputArray[i] = inputArray[i] * 2.0f;
        }
    }

    // 如果你的逻辑需要临时分配非托管内存(比如中间缓存),才需要这个释放函数
    // 但咱们当前方案用的是C#托管数组,所以这个函数可以不用!
    // __declspec(dllexport) void FreeTempMemory() {
    //     // 释放你在C++里new的内存
    // }
}

代码解释

  • extern "C":禁用C++的函数名 mangling,保证C#能通过函数名正确找到导出方法
  • __declspec(dllexport):标记函数为可导出,让DLL对外暴露这个接口
  • const float* inputArray:把输入数组设为只读,避免误修改原数据
  • float* outputArray:直接操作C#传入的托管数组内存,不需要拷贝,性能极高

Unity C#实现

接下来是C#侧的代码,包括纹理转数组、调用DLL、复用输出数组的逻辑:

using UnityEngine;
using System.Runtime.InteropServices;

public class TextureProcessor : MonoBehaviour
{
    // 导入C++ DLL的函数,注意CallingConvention要和C++一致(默认C++是cdecl)
    [DllImport("TextureProcessingDLL", CallingConvention = CallingConvention.Cdecl)]
    private static extern void ProcessTextureArray(
        [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] float[] inputArray,
        [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] float[] outputArray,
        int arrayLength
    );

    // 复用的输出数组,避免每次创建新数组触发GC
    private float[] _reusableOutputArray;
    // 纹理尺寸,假设你的动态纹理大小固定
    private const int TextureWidth = 2000;
    private const int TextureHeight = 2000;
    private int _arrayLength => TextureWidth * TextureHeight;

    void Start()
    {
        // 提前初始化复用的输出数组,只创建一次
        _reusableOutputArray = new float[_arrayLength];
    }

    void Update()
    {
        // 模拟动态生成的纹理(比如从RenderTexture获取)
        RenderTexture dynamicRT = GetYourDynamicRenderTexture();
        float[] inputArray = ConvertTextureToArray(dynamicRT);

        // 调用C++ DLL处理数组,直接修改_reusableOutputArray
        ProcessTextureArray(inputArray, _reusableOutputArray, _arrayLength);

        // 这里可以用处理后的_reusableOutputArray做后续逻辑,比如转成纹理显示
        UpdateTextureFromArray(_reusableOutputArray);
    }

    // 把动态纹理转成float数组的方法
    private float[] ConvertTextureToArray(RenderTexture rt)
    {
        Texture2D tempTex = new Texture2D(rt.width, rt.height, TextureFormat.RGBAFloat, false);
        RenderTexture.active = rt;
        tempTex.ReadPixels(new Rect(0, 0, rt.width, rt.height), 0, 0);
        RenderTexture.active = null;
        
        // 获取float类型的像素数组(每个通道0-1范围)
        float[] pixels = tempTex.GetPixels();
        Destroy(tempTex); // 销毁临时Texture2D,避免内存泄漏
        return pixels;
    }

    // 把处理后的数组转回纹理的方法(可选)
    private void UpdateTextureFromArray(float[] processedArray)
    {
        Texture2D resultTex = new Texture2D(TextureWidth, TextureHeight, TextureFormat.RGBAFloat, false);
        resultTex.SetPixels(processedArray);
        resultTex.Apply();
        
        // 比如把纹理赋值给材质
        GetComponent<Renderer>().material.mainTexture = resultTex;
    }

    // 模拟获取动态生成的RenderTexture
    private RenderTexture GetYourDynamicRenderTexture()
    {
        // 这里替换成你实际的动态纹理生成逻辑
        return RenderTexture.active;
    }
}

关键细节解释

  • [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)]:告诉Marshal把C#的托管数组映射成非托管指针,SizeParamIndex = 2指定第三个参数(arrayLength)是数组的长度,保证Marshal能正确处理数组边界
  • _reusableOutputArray:提前创建好的输出数组,每次调用DLL都复用它,避免频繁new数组导致的GC卡顿
  • ConvertTextureToArray:处理动态纹理转数组的逻辑,这里用RenderTexture转Texture2D再取像素,适合动态生成的纹理场景
  • 无unsafe代码:全程用Marshal的安全映射,完全符合你的兼容性要求

性能对比(参考你的测试数据)

对于400万元素的数组(2000x2000灰度纹理):

  • C++ DLL方法:约5ms
  • 纯C#循环方法:约23ms

差距主要来自C++的原生编译优化和无托管代码的额外开销,大数据量下优势非常明显。


内存管理注意事项

  1. 无需额外释放函数:因为我们操作的是C#的托管数组,内存由.NET GC管理,C++没有分配额外的非托管内存,所以不需要SomeC++MethodToDeAllocateMemory这样的函数,从根源避免了内存泄漏
  2. 数组长度要严格匹配:C++里不要越界访问数组,一定要保证arrayLength参数和C#侧数组的实际长度一致
  3. 纹理格式对应数组类型:
    • 如果用TextureFormat.RGBA32,对应byte[]数组(每个像素4个byte)
    • 如果用TextureFormat.RGBAFloat,对应float[]数组(每个像素4个float)
    • 如果是灰度纹理,可以用TextureFormat.RFloat对应单通道float[]

内容的提问来源于stack exchange,提问作者M.G.

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.15 04:35:06