将二进制数据作为字符串传递作为COM事件参数

3
我正在尝试将二进制数据从.NET传输到FoxPro(一种兼容COM的语言)。我有一个.NET对象,它是ComVisible的,并且具有带有字符串参数的事件接口。
在下面的示例中,我有一个虚拟实现,返回一个包含从0到255的每个连续字符的字符串。在.NET端,该字符串适当地存储每个未修改的字符,但在客户端处理事件时,128到154之间的字符被转换为问号。超过154个字符再次保持不变。
你知道是什么原因导致了这个问题吗?不幸的是,FoxPro没有本地表示二进制数据的方法,需要在字符串中传递它。
[Guid("974E3133-9925-4148-8A2B-F4B811072B17"), ComVisible(true), ComSourceInterfaces(typeof(IStreamEvents))]
public class DumbSerialPort {
    readonly string _buf;

    public event DataReceivedHandler DataReceived;
    public event EmptyDelegate Error;

    public DumbSerialPort() {
        var bbuf = new char[255];
        for (int c = 0; c < 255; c++)
            bbuf[c] = (char)c;

        _buf = new string(bbuf);
    }

    public void Fire() {
        if(DataReceived != null)
            DataReceived(_buf);
    }
}

[Guid("0F38F3C7-66B2-402B-8C33-A1904F545023"), ComVisible(true), InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
public interface IStreamEvents {
    void DataReceived(string data);
    void Error();
}

2
也许你应该将数据进行base64编码,而不是试图将其作为原始字符串使用?文本编码是一个复杂的问题,它并不是一种很好的编码数据的方式。 - Tim S.
Fxpro中的字符串类型是什么?BSTR? - Matt
@TimS。这是个好主意,我可能需要走这条路线,但我尝试让它工作的唯一原因是慢慢将代码迁移到.NET。有很多使用字符串作为二进制缓冲区的FoxPro代码,如果可以让它工作,我宁愿不修改它。 - joshperry
这个问题必须在FoxPro内部出错了。CLR将字符串转换为BSTR,这不会影响字符。例如,尝试使用Marshal.StringToBSTR()。因此,在FoxPro内部必须进行一种转换,相当于Encoding.Default.GetBytes()。这确实会破坏那些字符。Foxpro区分Varchar和Varchar(Binary),前者受代码页翻译的影响。 - Hans Passant
正是@HansPassant所说,由于COM仅使用Unicode,而FoxPro不符合Unicode规范,因此对来自COM对象的任何字符串数据进行翻译。我正在尝试操作接口,以使FoxPro省略其代码页翻译。 - joshperry
3个回答

4

问题

问题的原因有些复杂,根源在于FoxPro不支持Unicode字符串,而COM明确规定只能使用Unicode来处理字符串数据,因此传递带有不透明二进制数据的字符串会出现阻抗不匹配问题。

每当FoxPro调用返回或具有字符串参数的函数时,它都会在返回到用户代码之前进行代码页转换。这种转换显然会导致在字符串中隐藏的二进制数据移动时产生各种问题。

解决方案(有一个小问题)

嗯,byte[]应该可以解决问题,而且部分情况下也确实可以解决问题。但这个“部分”就是我试图将二进制数据隐藏在字符串中的原因所在。

以下是解决方案(我只验证过VFP 9 SP2,因为那是我正在使用的版本):在C# COM端,FoxPro可以处理以下定义的方法:

public byte[] GetData() { ... }

当调用该方法时,FoxPro将正确返回数据作为标记为二进制的字符串(请参见CreateBinary()以获取“标记二进制字符串”的说明)。这些字符串支持所有标准字符串操作函数,就像非二进制字符串一样;恰好是我所需要的。对于由FoxPro实现并传递给具有byte[]参数的C#的COM源事件接口,例如在我的原始示例中,也是如此。
为了使FoxPro不对发送到COM对象的字符串进行代码页转换,该字符串需要使用CreateBinary()函数创建,该函数将字符串标记为二进制并将绕过转换。
然而,FoxPro无法处理将“二进制”字符串传递给像这样定义的方法:
public void SendData(byte[] data) { ... }

如果您尝试调用此函数,将会出现无效的参数类型COM异常。
有几个原因导致此方法不能正常工作,基本上归结为FoxPro无法自动处理编组。
解决方法:
那么,我们该怎么做呢?定义一个像这样的函数。
public void SendData(object data) { ... }

好的,现在我们可以调用该函数,并使用标记的二进制字符串,FoxPro将不会进行任何代码页转换,数据将传输到.NET。但是data参数的数据类型是什么?它是一个System.Byte[*]。那个星号是什么意思?我一点也不清楚,所以我向SO上聪明的人们提问。
结果证明它是具有非零下限的数组。
因此,当我们从FoxPro获取二进制数据时,我们可以直接转换为byte[],除了FoxPro数组是基于1的这个事实。
因此,为了解决这个问题,这是我在C#中所做的:
public void SendData(object data) {
    byte[] buf = FPHelper.ToSZArray(data);
    // Use buf here
}

public class FPHelper {
    public static byte[] ToSZArray(object param) {
        var array = param as Array;

        if (array == null)
            throw new ArgumentException("Expected a binary array, (did you use CREATEBINARY()?)");
        if (array.Rank != 1)
            throw new ArgumentException("Expected array with rank 1.", "param");

        var dest = new byte[array.Length];
        Buffer.BlockCopy(array, 0, dest, 0, array.Length);

        return dest;
    }
}

在FoxPro中,唯一的要求是使用被标记为二进制的字符串来调用它:

cData = "Hello World!" + CHR(13) + CHR(12) + CHR(0)
oComObject.SendData(CREATEBINARY(cData))

2
虽然我对FoxPro的经验非常生疏,但我记得它可以将数组传递给COM对象,但是在接收它们方面存在问题。因此,考虑采用另一种方式,让Foxpro提供一个数组供C#填充,使用COMARRAY创建。从C#中,您会触发DataReceived事件并提供回调接口IProvideData。FoxPro将从其DataReceived事件处理程序内部调用它,并向您提供一个要填充的数组:
public interface IStreamEvents {
    void DataReceived(int count, IProvideData obj);
    void Error();
}

public interface IProvideArray {
    void ProvideData([In, Out] 
        MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UI1) byte[] buff);
}

当您在FoxPro端创建一个数组时,请记住以下几点(来自MSDN):
当您使用字节数组(VT_UI1)与COM服务器通信时,Visual FoxPro会将字节数组转换为字符串。加法nValue的值为1000保留了数组的原始类型,并且不会将结果转换为字符串。如果客户端通过引用将字节数组传递给Visual FoxPro COM服务器,则Visual FoxPro COM服务器还必须将nValue添加到1000。
在C#端,您只需处理一个数组:
public ProvideData(byte[] buff) {
    for (int c = 0; c < 255; c++)
        buff[c] = (byte)c;

}

public void Fire() {
    if(DataReceived != null)
        DataReceived(this); // this implements `IProvideArray`
}

0
StringBuilder stringB = new StringBuilder();
foreach (char c in asciiStr)
{
    uint ii = (uint)c;
    stringB .AppendFormat("{0:X2}", (ii & 0xff));
}
return stringB.ToString();

希望这可以帮到你


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接