C#调用以优化C ++ dll函数的行为

问题描述

经过为期一周的调试会议并搜索了问题之后,我问了我的朋友他的想法,幸运的是,他遇到了类似的问题,因此他指出了我要解决的问题,最终使我弄清楚了问题出在哪里,最后我设法找到了一个稳定的(希望)修复程序,但是我仍然不确定问题出在哪里(我猜不透,但稍后)。

首先,我有一个非托管的C ++ dll,可从C#中使用。基础结构(这是我重新创建的虚拟解决方案,仅用于重新创建和调试我在没有其他代码冲突的情况下遇到的问题),如下所示:

C ++代码:

    ===== device.h: =====

#pragma once

#include <cinttypes>

namespace abstraction
{
    class device final
    {
        public: uint32_t activity;
        public: uint32_t activities;
        
        public: device(uint32_t limit);
        
        public: bool execute();
    };
}

    ===== device.cpp =====

#include "device.h"

namespace abstraction
{
    device::device(uint32_t limit)
    {
        activity = 1u;
        activities = limit;
    }

    bool device::execute()
    {
        if (activity >= activities) return false;

        ++activity;

        return true;
    }
}

C ++ dll API:

        ===== api.h: =====

#pragma once

#include <abstraction/device.h> // it's the device above

#ifdef __TEST_API__
    #define __dll __declspec(dllexport)
#else
    #define __dll __declspec(dllimport)
#endif

#define dynamic extern "C" __dll

struct __dll Hndl
{
    uint64_t ptr;
};

// RC = ReinterpretCast
template <typename T> inline Hndl RC(T* obj) { return { reinterpret_cast<uint64_t>(obj) }; }
template <typename T> inline Hndl RC(T& obj) { return RC(&obj); }
template <typename T> inline T* RC(Hndl hndl) { return reinterpret_cast<T*>(hndl.ptr); }

// ===== Device API =====
dynamic Hndl CreateDevice(uint32_t limit);
dynamic void DisposeDevice(Hndl _device);
dynamic uint32_t DeviceActivity(Hndl _device);
dynamic uint32_t DeviceActivities(Hndl _device);
dynamic bool DeviceExecute(Hndl _device);

        ===== api.cpp: =====

#include "api.h"

using namespace abstraction;

// ===== Device API =====
dynamic Hndl CreateDevice(uint32_t limit)
{
    return RC(new device(limit));
}
dynamic void DisposeDevice(Hndl _device)
{
    delete RC<device>(_device);
}
dynamic uint32_t DeviceActivity(Hndl _device)
{
    return RC<device>(_device)->activity;
}
dynamic uint32_t DeviceActivities(Hndl _device)
{
    return RC<device>(_device)->activities;
}
dynamic bool DeviceExecute(Hndl _device)
{
    return RC<device>(_device)->execute();
}

C#API:

using System.Runtime.InteropServices;

namespace CSDllProject
{
    [StructLayout(LayoutKind.Sequential,CharSet = CharSet.Unicode)]
    public struct Hndl
    {
        public static Hndl Invalid = new Hndl(0ul);
        private ulong ptr;
        public ulong Ptr { get => ptr; internal set => ptr = value; }
        public bool ValidHandle => Ptr != 0ul;
        internal Hndl(ulong ptr) { this.ptr = ptr; }
        public override bool Equals(object obj) => base.Equals(obj);
        public override int GetHashCode() => base.GetHashCode();
        public static bool Equal(Hndl h1,Hndl h2) => h1.Ptr == h2.Ptr;
        public static bool operator ==(Hndl h1,Hndl h2) => Equal(h1,h2);
        public static bool operator !=(Hndl h1,Hndl h2) => !Equal(h1,h2);
    }

    public static class API
    {
#if DEBUG
        private const string TestApi = @"<path_to_debug_c++_dll>";
#else
        private const string TestApi = @"<path_to_release_c++_dll>";
#endif

        // ===== Device API =====
        [DllImport(TestApi,CallingConvention = CallingConvention.Cdecl,CharSet = CharSet.Unicode)]
        public static extern Hndl CreateDevice(uint limit);
        [DllImport(TestApi,CharSet = CharSet.Unicode)]
        public static extern void DisposeDevice(Hndl _device);
        [DllImport(TestApi,CharSet = CharSet.Unicode)]
        public static extern uint DeviceActivity(Hndl _device);
        [DllImport(TestApi,CharSet = CharSet.Unicode)]
        public static extern uint DeviceActivities(Hndl _device);
        [DllImport(TestApi,CharSet = CharSet.Unicode)]
        public static extern bool DeviceExecute(Hndl _device);
    }
}

C#包装器:

using System;

namespace CSDllProject
{
    public class Device : IDisposable
    {
        public Hndl Handle { get; private set; }

        public uint Activity => API.DeviceActivity(Handle);
        public uint Activities => API.DeviceActivities(Handle);

        public Device(uint limit) => Handle = API.CreateDevice(limit);
        ~Device() => Dispose();

        public bool Execute() => API.DeviceExecute(Handle);

        public void Dispose()
        {
            API.DisposeDevice(Handle);
            Handle = Hndl.Invalid;
        }
    }
}

C#单元测试:

using Microsoft.VisualStudio.TestTools.UnitTesting;

using CSDllProject;

namespace UnitTest.NET
{
    [TestClass] public class DeviceTests
    {
        [TestMethod] public void Test1()
        {
            using (var device = new Device(100u)) // [*]
            {
                do
                {
                    Extension.LogLine($"Device Activity: {device.Activity:0000}"); // just outputs this string to a log file,nothing fancy
                }
                while (device.Execute());
            }
        }
    }
}

接下来是这个小例子的目的。创建“设备”后(同样,这是一个暂存器,因此没有代码具有实际意义),您可以对其进行限制(可以用[*]标记)。在示例中,它是100u。然后,当您调用.execute()时,它将检查它是否已超过其可以执行的activities的数目。如果有,它将返回false。否则,它将增加activity(然后在原始项目中它实际上会做某事)并返回true,这意味着它“做了”一些有意义的事情。在调用方(C#)代码中,我引入了一个do-while循环,并且在这种情况下,我仅调用了.execute()方法。现在,一切正常,日志如下所示:

Device Activity: 0001
Device Activity: 0002
Device Activity: 0003
Device Activity: 0004
...
Device Activity: 0097
Device Activity: 0098
Device Activity: 0099
Device Activity: 0100

这是预期的。但是,如果我将活动限制([*]行)更改为1000u,则会发生一些奇怪的事情。我运行单元测试,它从不停止。因此,我强行将其停止并检查日志。日志现在看起来像这样:

Device Activity: 0001
Device Activity: 0002
Device Activity: 0003
Device Activity: 0004
...
Device Activity: 0997
Device Activity: 0998
Device Activity: 0999
Device Activity: 1000
Device Activity: 1000
Device Activity: 1000
Device Activity: 1000
Device Activity: 1000
Device Activity: 1000
Device Activity: 1000
Device Activity: 1000

起初我不知道该怎么想...直到1000年都做得不错,但是之后似乎并没有脱离循环(这就是单元测试从未完成的原因),但是,它也没有增加值(不,日志没有问题,因为它的实现效率很低,可以打开文件,写入文件,然后在每次调用时立即将其关闭,这样就可以100%工作)。

我已经在另一个C ++项目(device类本身没有dll的情况下测试了C ++代码,因为它实际上位于共享项目中)并且没有错误(调用代码与C#单元测试中的一项,它在100u1000u的限制下都表现出色)。 C#实际上有简单的调用,因此没有什么可以特别测试的。在我的原始项目中,在调试和释放模式下调用这段代码也有一些区别。这闻到了编译器可能会进行的某些优化可能会使代码混乱。

他对我的朋友说,在工作中他遇到了类似的问题,有一次他不得不将东西运送到经过调试编译的产品中。同样,有更多的理由认为优化可能是问题所在。我尝试了很多事情,最后,我在C ++ dll api中添加了一些日志记录:

dynamic bool DeviceExecute(Hndl _device)
{
    device* dev = RC<device>(_device);
    // log code that logs memory address of dev
    bool result = dev->execute();
    // log code that logs the result
    return result;
}

现在此代码在1000u的限制下可以正常工作。我尝试注释掉日志行,并保留其余代码,然后再次停止工作。这些都已经发布了。将其切换到调试模式会有不同的结果。有和没有日志行都可以工作。在这一点上,我绝对可以确定编译器会在发行时进行优化,以便具有:

dynamic bool DeviceExecute(Hndl _device)
{
    device* dev = RC<device>(_device);
    bool result = dev->execute();
    return result;
}

最终将变成:

dynamic bool DeviceExecute(Hndl _device)
{
    return RC<device>(_device)->execute();
}

无论如何,因为他可以得出结论,先前表达式的结果仅在一个表达式中使用,因此将其转换为第二个变量。最后,解决问题的方法是引入volatile来阻止编译器进行这种优化,并最终得到:

dynamic bool DeviceExecute(Hndl _device)
{
    volatile bool result = RC<device>(_device)->execute();
    return result;
}

现在,对于活动中的100u1000u限制,这在调试和发行时都可以正常工作。我的朋友告诉我,优化,堆栈,.NET调用非托管dll的方式等存在一些问题,但他甚至不知道它到底是什么,但是堆栈上有一些值(例如局部变量)可以帮助解决问题。这是我的问题,这到底是怎么回事?

解决方法

暂无找到可以解决该程序问题的有效方法,小编努力寻找整理中!

如果你已经找到好的解决方法,欢迎将解决方案带上本链接一起发送给小编。

小编邮箱:dio#foxmail.com (将#修改为@)