首页 > 解决方案 > 通过类型将字符数组从 VBA 传递到 Fortran DLL 会破坏其他类型成员

问题描述

信不信由你,这个标题是我能做到的,并且仍然描述了我遇到的问题!

所以这是场景:我从 VBA 调用 Fortran DLL,并且 DLL 使用用户定义的类型或任何 Fortran 名称(结构?)作为参数,并将类型复制回调用者进行验证。

该类型有一个固定长度的字符数组和一些整数。

我注意到在这个字符数组之后定义的任何属性中都有一些有趣的行为,我将在下面介绍我的简化测试设置之后:


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 返回具有相同属性 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 属性的大型程序中隔离是多么集中的乐趣。如果有人有任何想法,将不胜感激!

如果我不马上回复你,那是因为我要收工了,但我会尽量监控我的收件箱。

标签: excelvbafortrangfortranfortran90

解决方案


(这个答案是基于对 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

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

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)

推荐阅读