通过类型将字符数组从 VBA 传递到 Fortran DLL 会破坏其他类型成员

Passing a Character Array from VBA to Fortran DLL through a Type is corrupting the other Type members

信不信由你,这个标题尽可能短,但仍然描述了我遇到的问题!

所以情况是这样的:我从 VBA 调用 Fortran DLL,并且 DLL 使用 user-defined 类型或任何 Fortran 名称(结构?)作为参数,并且将类型复制回调用方以进行验证。

该类型有一个 fixed-length 字符数组和一些 运行 整数。

我注意到在这个字符数组之后定义的任何属性中有一些有趣的行为,我将在下面讨论,在我描述我的 boiled-down 测试设置之后:


Fortran 方面:

这是主要程序:

SUBROUTINE characterArrayTest (simpleTypeIn, simpleTypeOut)


           use simpleTypeDefinition
           

!GCC$ ATTRIBUTES STDCALL :: characterArrayTest


           type(simpleType),                INTENT(IN)     :: simpleTypeIn
           type(simpleType),                INTENT(OUT)    :: simpleTypeOut
           
         
           simpleTypeOut = simpleTypeIn
              
        
END SUBROUTINE characterArrayTest

这里是 simpleTypeDefinition 模块文件:

Module simpleTypeDefinition


  Type simpleType

     character (len=1)  :: CharacterArray(1) 
     !The length of the array is one here, but modified in tests
     
     integer   (kind=2) :: FirstInteger

     integer   (kind=2) :: SecondInteger

     integer   (kind=2) :: ThirdInteger

  End Type simpleType

  
End Module simpleTypeDefinition

编译步骤:

 gfortran -c simpleTypeDefinition.f90 characterArrayTest.f90
 gfortran -shared -static -o characterArrayTest.dll characterArrayTest.o

注意:这是 32 位版本的 gfortran,因为我使用的是 32 位版本的 Excel。


VBA方:

首先,镜像的 simpleType 和 declare 语句:

Type simpleType

    CharacterArray(0) As String * 1  
    'The length of the array is one here, but modified in tests
    
    FirstInteger As Integer
    
    SecondInteger As Integer
    
    ThirdInteger As Integer
    
End Type

Declare Sub characterArrayTest Lib "characterArrayTest.dll" _
Alias "characterarraytest_@8" _
(simpleTypeIn As simpleType, simpleTypeOut As simpleType)

接下来调用代码:

Dim simpleTypeIn As simpleType
Dim simpleTypeOut As simpleType

simpleTypeIn.CharacterArray(0) = "A"
'simpleTypeIn.CharacterArray(1) = "B"
'simpleTypeIn.CharacterArray(1) = "C"
'simpleTypeIn.CharacterArray(3) = "D"

simpleTypeIn.FirstInteger = 1
simpleTypeIn.SecondInteger = 2
simpleTypeIn.ThirdInteger = 3

Call Module4.characterArrayTest(simpleTypeIn, simpleTypeOut)

奇怪的越野车行为:

现在我们已经完成了设置,我可以描述发生了什么:

(我在玩弄字符数组的长度,同时将单个字符的长度设置为 1。我在所有情况下都匹配两侧的字符数组参数。)


测试用例:CharacterArray长度=1

对于第一种情况,一切正常,我从 VBA 传入 simpleTypeIn 和 simpleTypeOut,Fortran DLL 接受它并将 simpleTypeIn 复制到 simpleTypeOut,在调用 VBA returns 具有相同属性 CharacterArray、FirstInteger 等的 simpleTypeOut。


测试用例:CharacterArray长度=2

这就是事情变得有趣的地方。

在调用之前,simpleTypeIn 已定义。刚一打完,simpleTypeIn.ThirdInteger就从3变成了65!更奇怪的是,65 是字符 A 的 ASCII 值,即 simpleTypeIn.CharacterArray(0).

我通过将“A”更改为“(”来测试这种关系,它的 ASCII 值为 40,果然,simpleTypeIn.ThirdInteger 更改为 40。奇怪。

无论如何,人们会期望 simpleTypeOut 是 simpleTypeIn 变形后的任何奇怪事物的副本,但事实并非如此! simpleTypeOut 是 simpleTypeIn 的副本,除了 simpleTypeOut.ThirdInteger,它是 16961!


测试用例:CharacterArray长度=3

奇怪的是,这个案例与案例 2 相同。


测试用例:CharacterArray长度=4

在这种同样奇怪的情况下,调用后 simpleTypeIn.SecondInteger 从 2 变为 65,并且 simpleTypeIn.ThirdInteger 从 3 变为 66,这是 B 的 ASCII 值。

也不甘示弱,simpleTypeOut.SecondInteger出来的是16961,simpleTypeOut.ThirdInteger是17475。其他值复制成功(我反注释了B、C、D字符赋值,以匹配数组大小.)


观察:

这种奇怪的损坏似乎与字符数组中的字节成线性关系。我做了一些测试,如果有人想要在星期一使用长度为 2 而不是 1 的单个字符,我将进行编目,并且当数组的大小为 1 时发生损坏,而不是等到大小为 2。它也没有当数组的大小为 3 时,不要像 size = 1 的情况那样“跳过”额外的损坏。


这对我来说很容易成为名人堂的错误;我相信您可以想象在具有大量 Type 属性的 large-scale 程序中隔离是多么集中的乐趣。如果有人有任何想法,我们将不胜感激!

如果我没有立即回复您,那是因为我今天就到此为止,但我会尽量监控我的收件箱。

(此答案基于对Fortran的理解,而非VBA)

在这种情况下,在大多数情况下,Fortran 不会自动为您调整数组大小。当您引用字符数组的第二个元素时(使用 simpleTypeIn.CharacterArray(1) = "B"),该元素不存在且不会创建。

相反,代码尝试在字符数组的第二个元素(如果存在)的位置设置 的任何内存。在这种情况下,该内存似乎用于存储整数。

如果您完全忘记 VBA,您会发现同样的事情正在发生。这是一个完全用 Fortran 语言编写的示例代码,用于演示类似的行为:

enet-mach5% cat main.f90 
! ===== Module of types
module types_m
   implicit none

   type simple_t
      character(len=1) :: CharacterArray(1) 
      integer :: int1, int2, int3
   end type simple_t
end module types_m


! ===== Module of subroutines
module subroutines_m
   use types_m, only : simple_t
   implicit none
contains

! -- Subroutine to modify first character, this should work
subroutine sub1(s)
   type(simple_t), intent(INOUT) :: s

   s%CharacterArray(1) = 'A'
end subroutine sub1

! -- Subroutine to modify first and other (nonexistent) characters, should fail
subroutine sub2(s)
   type(simple_t), intent(INOUT) :: s

   s%CharacterArray(1) = 'B'
   s%CharacterArray(2:8) = 'C'
end subroutine sub2

end module subroutines_m


! ===== Main program, drives test
program main
   use types_m, only : simple_t
   use subroutines_m, only : sub1, sub2
   implicit none

   type(simple_t) :: s

   ! -- Set values to known
   s%int1 = 1
   s%int2 = 2
   s%int3 = 3
   s%CharacterArray(1) = 'X'

   ! -- Write out values of s
   write(*,*) 'Before calling any subs:'
   write(*,*) 's character: "', s%CharacterArray, '"'
   write(*,*) 's integers: ', s%int1, s%int2, s%int3

   ! -- Call first subroutine, should be fine
   call sub1(s)

   write(*,*) 'After calling sub1:'
   write(*,*) 's character: "', s%CharacterArray, '"'
   write(*,*) 's integers: ', s%int1, s%int2, s%int3

   ! -- Call second subroutine, should overflow character array and corrupt
   call sub2(s)

   write(*,*) 'After calling sub2:'
   write(*,*) 's character: "', s%CharacterArray, '"'
   write(*,*) 's integers: ', s%int1, s%int2, s%int3

   write(*,*) 'complete'

end program main

在这种情况下,我将模块和主程序放在同一个文件中。通常,人们会将它们保存在单独的文件中,但对于此示例来说没问题。我还必须设置 CharacterArray 的 8 个元素来显示错误,但确切的大小取决于系统、编译器和优化设置。 运行 这在我的机器上产生:

enet-mach5% gfortran --version
GNU Fortran (SUSE Linux) 4.8.3 20140627 [gcc-4_8-branch revision 212064]
Copyright (C) 2013 Free Software Foundation, Inc.

GNU Fortran comes with NO WARRANTY, to the extent permitted by law.
You may redistribute copies of GNU Fortran
under the terms of the GNU General Public License.
For more information about these matters, see the file named COPYING

enet-mach5% gfortran main.f90 && ./a.out
main.f90:31.20:

   s%CharacterArray(2:8) = 'C'
                    1
Warning: Lower array reference at (1) is out of bounds (2 > 1) in dimension 1
 Before calling any subs:
 s character: "X"
 s integers:            1           2           3
 After calling sub1:
 s character: "A"
 s integers:            1           2           3
 After calling sub2:
 s character: "B"
 s integers:   1128481603           2           3
 complete

Gfortran 足够聪明,可以标记 s%CharacterArray(2) 超出范围的编译时警告。您可以看到字符数组没有调整大小,而是 int1 的值被破坏了。如果我用更多的 运行 时间检查进行编译,我会得到一个完整的错误:

enet-mach5% gfortran -fcheck=all main.f90 && ./a.out
main.f90:31.20:

   s%CharacterArray(2:8) = 'C'
                    1
Warning: Lower array reference at (1) is out of bounds (2 > 1) in dimension 1
 Before calling any subs:
 s character: "X"
 s integers:            1           2           3
 After calling sub1:
 s character: "A"
 s integers:            1           2           3
At line 31 of file main.f90
Fortran runtime error: Index '2' of dimension 1 of array 's' outside of expected range (1:1)

看来我今天要(编辑: 不是)领取我自己的赏金!

这个问题的根源在于 VBA 每个字符占用 2 个字节,而 Fortran 期望每个字符占用 1 个字节。内存乱码是由字符数组占用的内存比 Fortran 预期的多 space 引起的。将 1 字节字符发送到 Fortran 的方法是这样的:


类型定义:

Type simpleType

    CharacterArray(3) As Byte

    FirstInteger As Integer

    SecondInteger As Integer

    ThirdInteger As Integer

End Type

从 VBA 字符到字节值的转换:

Dim tempByte() As Byte

tempByte = StrConv("A", vbFromUnicode)
simpleTypeIn.CharacterArray(0) = tempByte(0)

tempByte = StrConv("B", vbFromUnicode)
simpleTypeIn.CharacterArray(1) = tempByte(0)

tempByte = StrConv("C", vbFromUnicode)
simpleTypeIn.CharacterArray(2) = tempByte(0)

tempByte = StrConv("D", vbFromUnicode)
simpleTypeIn.CharacterArray(3) = tempByte(0)

此代码成功地将作为参数传递的字符串传递给 StrConv 函数。我测试了它们是否在 Fortran DLL 中转换为正确的 ASCII 字符,它们确实做到了!此外,整数不再被错误地传回!已标记名人堂错误。