Mixed-Language Windows Programming

Microsofts's Visual Basic makes it easy for Fortran programmers to access Windows 3 features.


October 01, 1991
URL:http://www.drdobbs.com/windows/mixed-language-windows-programming/184408643

Figure 1


Copyright © 1991, Dr. Dobb's Journal

OCT91: MIXED-LANGUAGE WINDOWS PROGRAMMING

MIXED-LANGUAGE WINDOWS PROGRAMMING

Fortran as a DLL, Visual Basic for the front end

John Norwood

John is a technician for Microsoft's Product Support group and can be reached at One Microsoft Way, Redmond, WA 98052-6399.


In the beginning, there was the punch card deck, the teletypewriter, and Fortran. Since then, the I/O interaction characteristics of Fortran have in many ways remained frozen, restricted by READ and WRITE, the only available ANSI standard I/O statements. But times change. Users are no longer willing to accept command-line, console-oriented user interfaces. Consequently, many Fortran programmers want to move from character-based screen management to graphical user interfaces such as Windows 3.0. With tools such as Microsoft's Fortran 5.1 QuickWin runtime library (which allows standard, console-oriented programs to be ported without modification to Windows), programmers can avoid the immediate plunge into the complexities of GUI programming.

But the QuickWin libraries represent a trade-off between ease of use and flexibility. The QuickWin user interface does not support features such as application-specific menus, buttons, or graphics. Therefore, to build Windows applications, Fortran programmers are expected to integrate Fortran code with a C Windows program that utilizes the Windows Software Development Kit.

Enter Visual Basic

C can present a serious programming challenge for the average Fortran programmer. Fortunately, Microsoft's Visual Basic provides an alternative with easy access to virtually all the sophisticated features of Windows. And if the standard feature set of Visual Basic is not sufficient or optimal for the task at hand, the programmer can make direct calls to the Windows kernel, create a custom Dynamic Link Library (DLL) using another language, or create a custom control using C and the Visual Basic Control Development Kit.

An area of interest to Fortran programmers, then, is the potential to take existing Fortran code, modify it for use in a DLL, and then access this DLL from Visual Basic, which serves as the front end to the Fortran routines. In this way, each language can contribute in its area of greatest strength.

Communicating through DLLs

The problems of mixed-language programming caused by runtime library conflicts during static linking aren't an issue, as DLLs are created and linked as stand-alone, single-language, modules. Because global data cannot be shared between an application and a DLL, all communication between Visual Basic and a Fortran Windows DLL is through the parameter list.

Visual Basic treats Fortran DLLs essentially as "black boxes," into which data is passed through the argument list, and out of which modified values are passed back through arguments or as function return values. All communication with the user must be through Visual Basic because a Fortran DLL cannot do any screen I/O (all input and output calls in a Fortran DLL are resolved by character-based routines and are incompatible with the Windows environment). Also, in keeping with the "black box" metaphor, a Fortran DLL cannot call back to routines in the Visual Basic main program. File I/O is possible from the Fortran DLL as long as care is exercised to close the file before the application terminates. All code in the Fortran DLL must consist of functions or subroutines because the Visual Basic program is the main program, or in Windows terms, the actual "task" (DLLs can never be tasks under Windows).

I find it best to initially test and thoroughly exercise the Fortran code with a main driver main program (either DOS or QuickWin). This eliminates the possibility of the Fortran/Visual Basic interface as the culprit in any suspicious program behavior. Also, the CodeView for Windows source-level debugger that comes with Microsoft Fortran can be used if symbolic information is included in the Fortran DLL.

To create the actual DLL, you must first create a definitions file to link it. A sample .DEF file called MATLIB.DEF, which can be used as a guide, is included with Fortran 5.1. The only modifications necessary are to change the name in the LIBRARY statement to match the base name of your DLL, and to include the names of all subroutines or functions that you plan to call directly from Visual Basic in the EXPORT statement. Always include the default WEP DLL termination routine in the EXPORT section of a DLL definitions file. Code that will be included in a DLL must be compiled with the /Aw option; code that will be an entry point into the DLL must be compiled with the /Aw and /Gw options. The link must use the runtime library LDLLFEW.LIB and the definitions file. It isn't necessary to create an import library using the IMPLIB utility because Visual Basic does not require it.

Accessing the DLL

A Visual Basic program is managed as a project which contains form modules where the layout of the user interface is designed; code modules containing auxiliary code; and a global module for declaration of elements of global scope. In order to access a DLL from Visual Basic, a Basic Declare statement with the special Lib keyword is required.

Fortran is particularly easy to interface with because the default calling protocols in both languages are closely matched: Almost everything in both languages is passed as a far pointer, and both languages store arrays in column major format. Modification is required only in passing strings and arrays. Strings in Visual Basic can be either dynamic or fixed-length. When communicating with any Fortran DLL, it is best to use fixed-length strings because a dynamic-length string is only as long the last value assigned to it. If this value is smaller than the length of the string as declared in the Fortran routine, the Fortran code will write out into nonallocated areas of memory, wreaking havoc with other memory locations or causing a protection violation. To pass a string from Visual Basic to any DLL, the keyword ByVal must be specified on the string argument in the Declare statement for that DLL routine. Passing strings as ByVal arguments is critical because it cancels the additional information that Visual Basic uses in passing strings to other Visual Basic routines and allows the correct generation of the calling protocols for a mixed-language interface. For example, if the Fortran routine looks like that in Example 1(a), the Visual Basic sub should look like Example 1(b), and the declaration in the Declarations section of the form or module should resemble 1(c).

Example 1: Passing strings: (a) Fortran routine; (b) Visual Basic sub; (c) declaration.

  (a)

  SUBROUTINE STRINGER (INSTRING)
  CHARACTER*40 INSTRING
  INSTRING = 'This is from Fortran'
  END

  (b)

  Sub Form_Click ()
    Dim temp As String * 40
    Call STRINGER (temp)
    Debug.Print temp
  End Sub

  (c)

  Declare Sub STRINGER Lib "d:\vb\
   test\string.dll" (ByVal Mystring
                         As String)

Care must also be exercised in passing arrays as parameters. Visual Basic has some special array handling facilities that aid in passing arrays from one Visual Basic routine to another. This default handling of arrays must be suppressed when communicating with other languages. When an array is passed from Visual Basic to a DLL, the first element of the array is specified in the call. This results in a far pointer to the head of the array being passed to the routine in the Fortran DLL, which is precisely what Fortran expects to receive when an array is passed. For example, if the Fortran routine looks like Example 2(a), the Visual Basic sub will look like Example 2(b), and the declaration in the Declarations section of the form or module will look like Example 2(c).

Example 2: Passing arrays: (a) Fortran routine; (b) Visual Basic sub; (c) declaration.

  (a)

  SUBROUTINE ARRAYTEST (ARR)
  INTEGER*4 ARR(20)
  ARR = 5
  END

  (b)

  Sub Form_0Click ()
    Static testarray (1 To 20) As
                             Long
    Call ARRAYTEST(testarray(1))
    For i% = 1 To 20
      Debug.Print testarray(i%)
    Next i%
  End Sub

  (c)

  Declare Sub ARRAYTEST Lib "d:\vb\
  test\array.dll* (Myarray As Long)

Finally, one of the more difficult things to pass from Visual Basic to a DLL is an array of strings. The solution is to declare a user-defined type in Visual Basic with only a fixed-length string as its element. This will map onto an array of strings in Fortran. For example, if the Fortran routine looks like Example 3(a), the Visual Basic sub will look like Example 3(b), and the declarations in the global module will look like Example 3(c).

Example 3: Passing an array of strings: (a) Fortran routine; (b) Visual Basic sub; (c) declaration.

  (a)

  SUBROUTINE ARRAYSTRINGS (ARR)
  CHARACTER*24 ARR(5)
  ARR = 'This is a string also'
  END

  (b)

  Sub Form_Click ()
    Static testarray (1 To 5) As
                        StringArray
    Call ARRAYTEST (testarray(1))
    For i% = 1 To 5
      Debug.Print
  testarray(i%). strings
    Next i%
  End Sub

  (c)

  Type StringArray
     strings As String * 24
  End Type

  Declare Sub ARRAYSTRINGS Lib "d:\
     vb\test\array.dll" (Myarray As
                       StringArray)

Declare statements can be placed in the Declarations section of any form or module but the Type statement can only be placed in the global module. Passing all other data items is very straightforward. Note, however, that Fortran should never use the construct CHARACTER*(*) to receive adjustable size strings into a DLL, although there is no problem with using adjustable size arrays in a Fortran DLL.

Other Considerations

Before describing a sample application, I'll address some limitations of mixed Visual Basic and Fortran programming. Visual Basic does not support data items greater than 64 Kbytes in size -- huge data items in Microsoft language terms.

Microsoft Fortran routines can create and manipulate such items, but Visual Basic will only be able to access them either element by element or in pieces less than 64 Kbytes. There is no easy solution to this problem if huge data must be used. C DLLs have been written that provide a whole suite of services for working with huge arrays. All the array allocation and manipulation are done by calls from Visual Basic to the C service routines. An equivalent set of services written in Fortran would be difficult to emulate due to the lack of pointer capability.

I've already touched on another mixed Visual Basic and Fortran development limitation. Fortran must be accessed as a DLL, so it must be a passive receiver of information from Visual Basic. In many cases, existing Fortran code containing I/O scattered throughout would have to be extensively modified to work under these conditions. Using C and the Windows SDK with Fortran isn't an easy solution because although Fortran need not be accessed as a DLL, all screen I/O and calls to the Windows API must still be done from C. This still results in a great deal of recoding on the Fortran side. The one advantage of using C is that the Fortran code, if statically linked to a C application, can make callbacks to C routines in the main program or other C functions; this cannot be done from Fortran to Visual Basic.

Also, Fortran DLLs do not, by default, yield to any other process. Once a calculation begins in Fortran it will continue to its conclusion without stopping. This is not compatible with using the application in a multitasking mode.

Finally, there is the question of performance and flexibility. Performance of a GUI application is not an issue of benchmarks but of perception: How responsive does the application seem to the user? The advantage of doing the intensive number-crunching in an optimized Fortran DLL combined with an efficient design of the Visual Basic front end should result in an application whose performance is comparable to other Windows applications. For total flexibility nothing can compare to coding at the API level using C and the Windows SDK; however, the possibilities of extending Visual Basic using custom controls and direct calls to the Windows kernel help to diminish any perceived restrictions. When comparing the programming process in Visual Basic and programming using the Windows SDK, the cost/benefit ratio is completely situation-specific and dependent on the programming expertise and resources available.

Building an Eigenvalue Calculator

When I first thought of testing a mixed Visual Basic/Fortran example program, I looked for Fortran code that was well-tested, concise, nontrivial, and well-suited for utilization as a DLL. The book Numerical Recipes: The Art of Scientific Computing (Press, Flannery, Teukolsky, and Vetterling, Cambridge University Press, ISBN 0-521-30811-9) seemed like a good place to start. I thought Visual Basic would make matrix data entry easy to implement. Because Eigenvalues and Eigenvectors mathematically characterize any matrix for which they exist, I decided to use the Eigenvalue and Eigenvector computation routines (not too complex, yet not numerically trivial) to build an Eigenvalue calculator. Also, symmetric matrices suggest a user entry routine that enforces the matrix symmetry automatically, thus making the Visual Basic side of the program more interesting. Figure 1 shows the calculator. The Fortran routine TRED2 (Listing One, page 130) takes a symmetric matrix and returns a symmetric tridiagonal matrix in two vectors and an orthogonal matrix used in finding the Eigenvectors. The routine TQLI takes the output of TRED2 and uses orthogonal transformations with implicit shifts to produce a vector containing the Eigenvalues and a matrix with Eigenvectors for columns (details are in Numerical Recipes).

On the Visual Basic side, I had to decide which control was appropriate for a matrix entry routine. I chose the custom control GRID.VBX because it was designed for precisely this purpose. This control is like a small fragment of an Excel spreadsheet that can be easily resized and comes with built-in scroll bars and clipboard functionality. It does not consist of text entry fields so text entry must be done in a dedicated text control and transferred to the cells on the grid (just like in Excel). Some attempt was made to validate user entry, but this could have been more rigorous. The same grid that the matrix was entered on also displayed the Eigenvectors after calculation, and a one-dimensional row grid was used to display the corresponding Eigenvalues. The dimension of the grids was made dynamically resizable, ranging from 1 to 35. A simple menu was constructed to allow pasting to and copying from the grids to the Windows clipboard. Everything was contained on one form; the Global.bas module was used to declare various constants, and the Declare statements were used for communicating with the Fortran DLL.

Because the Fortran code was chosen with an eye to creating a DLL, it required little modification. One additional argument was added to the TQLI subroutine to pass out an error flag, and an error output statement was eliminated. The routines were left as separately exported subroutines instead of being called from within the DLL from a master subroutine. Having separately callable routines allows Visual Basic to regain control of the program more often because Fortran DLLs do not cooperate in multitasking and only relinquish control of the operating system on return. If desired, some code could be inserted to allow the user to cancel the operation before the second call to the DLL. The two routines were tested with a driver program and then made into a DLL using the menu options in the "Programmer's Workbench." The definition file was created by modifying the example .DEF file included with the Fortran 5.1 sample source code.

All the remaining work was on the Visual Basic side of the program. A calculator-style interface was used because it was straightforward to implement. There is a two-way connection between the grid text entry field and the matrix grid (grid1). This is where all matrix values are entered, modified, and validated. Maintaining this relationship and validating data entry were probably the least obvious aspects of the interface.

The remainder of the interface is mostly button-oriented and fairly self-evident. Two menus were created to allow the user to copy from and paste to the grids. This is where the power of the grid custom control was apparent. Every grid has a clip property that is robust in accepting data from the clipboard: If the quantity of data in the clipboard and the selected area don't match, data is automatically truncated or padded with nulls to make a match with no error invoked. This, combined with the default selection capability of a grid control (similar to that of an Excel spreadsheet) makes moving rows, columns, entire matrices, and subsets thereof extremely simple.

Probably the least satisfactory aspect of the program is the lack of features in navigating the matrix grid. There is no default way to advance to the next cell on a grid, although arrow keys and most other cursor movement keys are recognized when the grid is the focus. To implement this, a key (for example a tab) would have to be trapped in the text entry field and position on the grid dynamically maintained. If scrolling is necessary, this must also be maintained by the programmer. For the sake of simplicity, this was not implemented. Considering the powerful properties imbedded in the grid control, in any serious program this would not be much of an obstacle.

Data validation was done in a somewhat quick and dirty manner: The intrinsic Val function was used to convert whatever text was entered into a number and then back into text for placement in the grid. An attempt was made to notify the user when conversions were made. The sample program takes advantage of the convenient array handling features of Visual Basic. It was possible to dynamically allocate only as much memory as required for the current dimension on the form. Fortran programmers will certainly appreciate the ability to declare an array to be global to the form with global access to upper and lower bounds and yet dynamically allocate the array in Basic subroutines.

Gotchas

Transferring programming experience with a sequential procedural language such as Fortran to an event-driven programming system such as Visual Basic can lead to certain pitfalls. It is startlingly easy to throw up a series of controls that perform the required actions in isolation. Yet difficult and subtle programming and design issues lie latent in the interaction between controls and the free-form sequence of user actions allowed by a GUI. The user's ability to manipulate any control at any time (and often in a variety of ways) presents the programmer with an embedded sequence of temporal and positional "ifs" that if laid out in actual code, would manifest considerable complexity.

On-the-fly programming in Visual Basic without any prior planning or design is tempting, but can result in ground-up code rewrites due to unforeseen and subtle interactions between event handlers: Ease of programming partially masks the complexity of the user interface.

Visual Basic can be a very modularized programming system, yet all properties and methods of all controls are globally available. Hence, side effects are an ever-present danger. I often found myself wishing for some means of determining exactly which control previously had the focus. This is usually a sign that procedural programming habits have crept in and event handlers are not being coded in a truly self-contained manner.

An overpowering desire to add just one more global flag is evidence that old programming methods die a hard and lingering death. I was never able to eliminate all my own global variables because of the initial sloppy design of the program.

Wrapping Up

Every hard-core Fortran programmer probably has at least one bad Visual Basic program waiting to be written. Hopefully, the methodologies and paradigms of an event-driven interface will begin to be as fundamental and natural as the use of structured programming and well-documented code. The ease of interaction between programming systems such as Visual Basic and procedural languages means that useful Fortran code can exist as a component of a modern GUI program, thus preserving the value of existing code, and simplifying the transition to newer styles of programming.


_MIXED-LANGUAGE WINDOWS PROGRAMMING_
by John Norwood



[LISTING ONE]



      subroutine tred2(a,n,np,d,e)
c
c Householder reduction of a real symmetric, nxn matrix a, stored in an
c npxnp physical array.  On output, a is replaced by the orthogonal matrix
c q effecting the transformation.
c d returns the diagonal elements of the tridiagonal matrix, and e the off-
c diagonal elements, with e(1)=0.

      implicit none

      integer*4 i,j,k,l      ! Loop indices
      integer*4 n            ! Actual array size
      integer*4 np           ! Physical array size of incoming array
      real*4    a(np,np)     ! Matrix, np is used so dimensions match
      real*4    d(np)        ! Vector that will have diagonal elements
      real*4    e(np)        ! Vector that will have off-diagonal elements
      real*4    h            ! Vector norm used in forming projection
      real*4    scale        ! Scale factor
      real*4    f            ! Temporary variable
      real*4    g            ! Temporary variable
      real*4    hh           ! Another piece of the projection

      if (n.gt.1) then
       do 18 i=n,2,-1
         l=i-1
         h=0.
         scale=0.
         if(l.gt.1) then
           do 11, k=1,l
             scale=scale+abs(a(i,k))
11         continue
           if(scale.eq.0.) then       ! Skip transformation
             e(i)=a(i,l)
           else
             do 12, k=1,l
               a(i,k)=a(i,k)/scale    ! Use scaled a's in transformation
               h=h+a(i,k)**2          ! for eigenvectors
12           continue
             f=a(i,l)
             g=-sign(sqrt(h),f)
             e(i)=scale*g
             h=h-(f*g)
             a(i,l)=f-g
             f=0.
             do 15,j=1,l
               a(j,i)=a(i,j)/h
               g=0.
               do 13, k=1,j
                 g=g+a(j,k)*a(i,k)
13             continue
               if(l.gt.j) then
               do 14,k=j+1,l
                 g=g+a(k,j)*a(i,k)
14             continue
               endif
               e(j)=g/h               ! Form element of projection in
               f=f+e(j)*a(i,j)        ! temporarily unused element of e
15           continue
             hh=f/(h+h)
             do 17, j=1,l
               f=a(i,j)
               g=e(j)-hh*f
               e(j)=g
               do 16, k=1,j                    ! Loop to reduce matrix a
                 a(j,k)=a(j,k)-f*e(k)-g*a(i,k)
16             continue
17           continue
             endif
           else
             e(i)=a(i,l)
           endif
           d(i)=h
18       continue

c This starts the eigenvector specific part of the code.

       endif
       d(1)=0.
       e(1)=0.
       do 23, i=1,n          ! Begin accumulation of transformation matrices
         l=i-1
         if(d(i).ne.0.) then
           do 21, j=1,l
             g=0.
             do 19, k=1,l          ! Use information stored in a to form
               g=g+a(i,k)*a(k,j)   ! projection times orthogonal matrix
19           continue
             do 20, k=1,l
               a(k,j)=a(k,j)-g*a(k,i)
20           continue
21         continue
         endif

c This ends the eigenvector specific part of the code.

         d(i)=a(i,i)

c This starts the eigenvector specific part of the code.

         a(i,i)=1.            ! Reset row and column of a to identity matrix
         if(l.ge.1)then       ! for the next iteration of transformation loop
           do 22,j=1,l
             a(i,j)=0.
             a(j,i)=0.
22         continue
         endif

c This ends the eigenvector specific part of the code.

23     continue
       return
       end

      subroutine tqli(d,e,n,np,z,iterflag)

c This subroutine performs a QL algorithm with implicit shifts, to determine
c the eigenvalues and eigenvectors of a real, symmetric, tridiagonal matrix,
c or of a real, symmetric matrix previously reduced by tred2 above.
c d is a vector of length np.  On input, its first n elements are the
c diagonal elements of the tridiagonal matrix.  On output, it returns the
c eigenvalues.  The vector e inputs the subdiagonal elements of the
c tridiagonal matrix, with e(1) arbitrary. On output, e is destroyed.
c If eigenvectors are desired, the matrix z (nxn stored in an npxnp array)
c is input as the identity matrix or the matrix that is returned from tred2.
c On output, the kth column of z contains the normalized eigenvector
c corresponding to the eigenvalue in d(k).

      implicit none

      integer*4 i,k,l       ! Loop indices
      integer*4 iter        ! Iteration counter
      integer*4 n           ! Logical size of array z
      integer*4 m           ! Submatrix size
      integer*4 np          ! Physical size of array z
      integer*4 iterflag    ! Flag to return error code to main routine
      real*4    d(np)       ! Diagonal elements is, eigenvalues out
      real*4    e(np)       ! Off diagonal elements in, nothing out
      real*4    z(np,np)    ! Matrix from tred2 in, eigenvectors out
      real*4    dd          ! Holds small subdiagonal element
      real*4    g           ! Holds Givens rotation
      real*4    s           ! "Sin" component of Givens rotation
      real*4    c           ! "Cos" component of Givens rotation
      real*4    p           ! Element of projection matrix
      real*4    f           ! Holds result of "sin" applied to element of e
      real*4    b           ! Holds result of "cos" applied to element of e
      real*4    r           ! Temporary piece of c or s


      if(n.gt.1) then
        do 11, i=2,n                 ! Renumber elements of e
          e(i-1)=e(i)
11      continue
        e(n)=0.
        do 15,l=1,n
          iter=0
1         do 12,m=l,n-1              ! Search for small subdiagonal element
            dd=abs(d(m))+abs(d(m+1))
            if (abs(e(m))+dd.eq.dd) go to 2
12        continue
          m=n
2         if(m.ne.l) then

            if(iter.eq.30) then
             iterflag = -1
             return
            endif

            iter=iter+1
            g=(d(l+1)-d(l))/(2.*e(l))   ! Calculate shift
            r=sqrt(g**2+1.)
            g=d(m)-d(l)+e(l)/(g+sign(r,g))
            s=1.
            c=1.
            p=0.
            do 14, i=m-1,l,-1           ! Plane rotation followed by a Givens
              f=s*e(i)                  ! rotations to maintain tridiagonal
              b=c*e(i)                  ! form
              if(abs(f).ge.abs(g))then
                c=g/f
                r=sqrt(c**2+1.)
                e(i+1)=f*r
                s=1./r
                c=c*s
              else
                s=f/g
                r=sqrt(s**2+1.)
                e(i+1)=g*r
                c=1./r
                s=s*c
              endif
              g=d(i+1)-p
              r=(d(i)-g)*s+2.*c*b
              p=s*r
              d(i+1)=g+p
              g=c*r-b

c Start of code specific to forming eigenvectors.

              do 13, k=1,n
                f=z(k,i+1)
                z(k,i+1)=s*z(k,i)+c*f
                z(k,i)=c*z(k,i)-s*f
13            continue

c End of code specific to forming eigenvectors.

14          continue
            d(l)=d(l)-p
            e(l)=g
            e(m)=0.
            go to 1
          endif
15        continue
        endif


        return
        end




Example 1:

(a)

       SUBROUTINE STRINGER(INSTRING)
       CHARACTER*40 INSTRING
       INSTRING = 'This is from Fortran'
       END

(b)



Sub Form_Click ()
  Dim temp As String * 40
  Call STRINGER(temp)
  Debug.Print temp
End Sub

(c)

Declare Sub STRINGER Lib "d:\vb\test\string.dll" (ByVal Mystring As String)




Example 2:

(a)
       SUBROUTINE ARRAYTEST(ARR)
       INTEGER*4 ARR(20)
       ARR = 5
       END

(b)

Sub Form_Click ()

  Static testarray(1 To 20) As Long
  Call ARRAYTEST(testarray(1))
  For i% = 1 To 20
    Debug.Print testarray(i%)
  Next i%
End Sub



(c)
Declare Sub ARRAYTEST Lib "d:\vb\test\array.dll" (Myarray As Long)




Example 3

(a)

      SUBROUTINE ARRAYSTRINGS(ARR)
      CHARACTER*24 ARR(5)
      ARR = 'This is a string also'
      END

(b)

Sub Form_Click ()
  Static testarray(1 To 5) As StringArray
  Call ARRAYTEST(testarray(1))
  For i% = 1 To 5
    Debug.Print testarray(i%).strings
  Next i%
End Sub



(c)

Type StringArray
   strings As String * 24
End Type

Declare Sub ARRAYSTRINGS Lib "d:\vb\test\array.dll" (Myarray As StringArray)


Copyright © 1991, Dr. Dobb's Journal

Terms of Service | Privacy Statement | Copyright © 2024 UBM Tech, All rights reserved.