• Welcome to Powerbasic Museum 2020-B.
 

News:

Forum in repository mode. No new members allowed.

Main Menu

C# string to a PB-dll??

Started by Heinz Grandjean, November 05, 2013, 05:57:07 AM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Heinz Grandjean

How can I send a C# -string to a PB-dll

The code in C# (VS 2012)

using System;

using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.IO;
using System.Text;


namespace ConsoleApplication2
{
    class Program
    {
        [DllImport ("D:/ESTWGJ#_Quell/PB_DLL/DLL1.DLL")]
        public static extern int testme( ref string mytext);

        static void Main(string[] args)
        {
            int y = 0;
            string mytext = "Xx";

            y = testme( ref mytext );
            Console.WriteLine(y);
            Console.WriteLine();
        }
    }
}


The code of the function in a PB_dll:

FUNCTION NetTest ALIAS "testme"( _
                               BYREF wstrText AS WSTRING _
                               ) EXPORT AS LONG

       ? STR$(LEN (wstrText))
       ? wstrText

       FUNCTION = 1
END FUNCTION   


The calling of the dll is okay; I can read the PB return-value in C#.
But the W-string in PB seems to be useless???

Thanks,
Heinz Grandjean

Edwin Knoppert

The declaration 'ref string' may be something different in .net.
A string is an object and not a bstr.
I assume you must extend that declare with wchar lookalikes, which i don't know myself.
There are several approaches but if you like this..., this is the mmost obvious scenario so let's see how far you get.
Types between non .net apps are pretty daunting, been busy with structs myself :)

Search for "Default Marshaling for Strings"
( http://msdn.microsoft.com/en-us/library/s9ts558h.aspx )

Patrice Terrier

#2
Heinz

Give a look to my zExplorer project here:
http://www.zapsolution.com/DW/US/ccorner.html

It embeds a Folder Browser (written in PB) directly inside of your form, to mimic Windows Explorer.

Much string exchange in this project between PB and C#.

Warning it is UNSAFE code, of course :)
Patrice Terrier
GDImage (advanced graphic addon)
http://www.zapsolution.com

Edwin Knoppert

Afaik returning a string per function will leak.
Years ago discussed and afaik you even mentioned it returns it like an asciiz and thus the actual bstr handle is not released.
Did you investigate this then?

Daniel Corbier

You need to perform string marshaling when passing strings between .NET compilers (like C#), and native compilers (like PB).

Also, as Edwin mentioned, there's a problem if you rely on a string function's return value, especially since a return string can't be marshaled the way you can a string arg.  This can create a memory corruption issue that is hard to detect and may go unnoticed for entire sessions, but may pop up in other sessions when you least expect it.  So the solution is to return a string as a ByRef arg; you then can create a wrapper function after that in C# that returns a string as you originally intended.

I do it like this example, which covers both string arguments and a string return value :

PowerBASIC DLL "function":

Sub ucEvalStr Alias "ucEvalStr" (ByRef ReturnStr As String, ByRef Expr As String, Optional ByVal ExprType As Dword, Optional ByVal t As Dword) Export
   ' ...
End Sub


C#

[DllImport("uCalc32v380.dll", EntryPoint="ucEvalStr")] public static extern void ucEvalStr_([In,Out,MarshalAs(UnmanagedType.AnsiBStr)]ref string ReturnStr, [In,Out,MarshalAs(UnmanagedType.AnsiBStr)]ref string Expr, int ExprType , int t );
public static string ucEvalStr( string Expr, int ExprType = 0, int t = 0) {
   string ReturnStr = "";
   ucEvalStr_(ref ReturnStr, ref Expr,  ExprType ,  t );
   if(ReturnStr==null) {return "";} else return ReturnStr;
}


Note: If you are using PB's wide strings for handling Unicode (instead of PB's plain String), then replace UnmanagedType.AnsiBStr with UnmanagedType.BStr in the above example.

I tried many other alternatives, all of which had one problem or another.
Daniel Corbier
uCalc Software

Edwin Knoppert

#5
To add, i ever wrote me a BSTR pointer to .NET string so that you can use the PB BSTR returned.
It's fairly simple, returning a bstr is simply returning its handle which PB does of course.
In c# you could use int32 or intptr for example to copy the BSTR contents to a .NET object like byte array or something else.

Then you must free the handle (rule: the caller destroys).

Btw.., i did actually test the result/memory loss.

Mike Stefanik

Just wanted to point out that you should never pass pointers or handles around in an Int32 type. Always use IntPtr, that's what it's there for. Writing your code with the assumption that pointers and handles are always 32 bits wide will screw you to the post, eventually.

Mike Stefanik
sockettools.com

Daniel Corbier

Edwin, I think I know what you mean.  Using the same PowerBASIC exported function (ucEvalStr), here's what I had to do for Visual C++ native (which I do very differently in Visual C++.NET):

#pragma once
#include "OleAuto.h"
#include <wtypes.h>
#include <string>
using namespace std;

// ...

typedef void (CALLBACK* __ucEvalStr)(BSTR& ReturnStr, BSTR& Expr, __int32 ExprType, __int32 t  ); extern __ucEvalStr _ucEvalStr_; __ucEvalStr _ucEvalStr_;

string ucEvalStr(string Expr, __int32 ExprType  = 0, __int32 t  = 0) {
   BSTR ReturnStr_ = SysAllocStringByteLen(NULL, 1);
   BSTR Expr_ = SysAllocStringByteLen(Expr.c_str(), Expr.length());
   if(ucInit==0) uCalcInit();
   _ucEvalStr_(ReturnStr_, Expr_, ExprType, t  );
   string ReturnStr = (LPCSTR)ReturnStr_;
   SysFreeString(Expr_);
   SysFreeString(ReturnStr_);
   return ReturnStr;
}

__int32 uCalcInit() {
   static HINSTANCE hDLL;
#ifdef UNICODE
   hDLL = LoadLibrary(_T("uCalc32v380.dll"));
#else
   hDLL = LoadLibraryA("uCalc32v380.dll");
#endif
   
   if (hDLL == NULL) return 0;
   ucInit = 1;   

// ...
   _ucEvalStr_ = (__ucEvalStr)GetProcAddress(hDLL, "ucEvalStr");
// ...
   return 1;
}


Is it something like this you did?  In C# all of that is not necessary if you just let C# handle string marshaling.

Mike, your response came in as I'm typing.  Good advice.  I use __int32 above, but it's auto-generated.  I auto-generate different headers for the 64-bit DLLs.

All my headers for various languages are auto-generated using uCalc Transform.  There's no way I'd have time to manually write out each function like this for each revision of each product for each different compiler my uCalc products support.  I just run a batch file that calls uCalc Transform, and it quickly auto-generates up-to-date headers for all the supported compilers.

Daniel Corbier
uCalc Software

Heinz Grandjean

#8
At first I have to say thank you for your help, Patrice and Edwin.

I have proceeded a little bit. During writing, the three last messages came in and for the moment I can't 
answer to them.

I now can send a C#-string to a PB-DLL function and read it there.
In the next step I found out how to manipulate a C#-string inside a PB-DLL's function.
The complete code ( VS 2012):

using System;
using System.Runtime.InteropServices;
using System.Collections.Generic;
using System.IO;
using System.Text;
   

namespace ConsoleApplication2
{
    class Program
    {

        private const string dll1 = @"D:/ESTWGJ#_Quell/PB_DLL/DLL1.DLL";

        [DllImport(dll1, EntryPoint = "testme")]
        public static extern int NetTest([MarshalAs(UnmanagedType.AnsiBStr)] string mytext);

        [DllImport(dll1, EntryPoint = "testme2")]
        public static extern void NetTest2([MarshalAs(UnmanagedType.LPStr)] StringBuilder MyStringBuilder);

        static void Main(string[] args)
        {
            int y = 0;
          //sending a string to a PB_DLL-function:
          //=======================================
            string mytext = "Xx";
            y = Program.NetTest(mytext);
            Console.WriteLine(y);
            Console.WriteLine();

          //manipulating a string inside a PB_DLL-function:
          //===============================================
            StringBuilder MyStringBuilder = new StringBuilder("Hello World!", 256);
            Console.WriteLine(MyStringBuilder);
            NetTest2(MyStringBuilder);
            Console.WriteLine(MyStringBuilder);
        }
    }
}


The PB-DLL-code (PB 10.4):

#COMPILE DLL
#DIM ALL

#INCLUDE ONCE "Win32API.inc"

GLOBAL ghInstance AS DWORD

FUNCTION LIBMAIN (BYVAL hInstance   AS LONG, _
                  BYVAL fwdReason   AS LONG, _
                  BYVAL lpvReserved AS LONG) AS LONG

    SELECT CASE fwdReason

    CASE %DLL_PROCESS_ATTACH
        ghInstance = hInstance
        FUNCTION = 1   'success!
       'FUNCTION = 0   'failure!  This will prevent the EXE from running.

    CASE %DLL_PROCESS_DETACH
        FUNCTION = 1   'success!
       'FUNCTION = 0   'failure!

    CASE %DLL_THREAD_ATTACH
        FUNCTION = 1   'success!
       'FUNCTION = 0   'failure!

    CASE %DLL_THREAD_DETACH
        FUNCTION = 1   'success!
       'FUNCTION = 0   'failure!
    END SELECT

END FUNCTION


FUNCTION NetTest ALIAS "testme"( _
                               BYREF strText AS ASCIIZ * 256 _
                               ) STATIC EXPORT AS LONG

       ? STR$(LEN (strText))
       ? strText

       FUNCTION = 2222
END FUNCTION

SUB NetTest2 ALIAS "testme2"( _
                            BYREF strText AS ASCIIZ * 256 _
                            ) STATIC EXPORT
       ? strText
         strText = LEFT$(strText,11) + " has been severely changed!"
       ? strText
END SUB                 


I'm not successful using a PB function that has the return value as string.
The C# application crashes as Edwin and Daniel have mentioned.
It would be interesting if there is any solution.

Patrice, I checked your example. It is interesting (for my inexperienced eyes??)
that you use the C# strings in the way that blocks here in VS2012. But maybe I have overseen something.
Sorry.
Finally, please allow me a word of explanation.
In case of the major problem we all are faced with I intensely try to escape,..
Because my application and it's DLLs is not that small (about 300.00 lines in PB-code) I need a way to
convert it part by part.
And naturally the way is cross-platform...
Following Patrice, I learned C++ only to realize that VS 2012 does no longer provide a GUI-toolkit for C++-
Doing everything in SDK-style would be too time consuming for me.
Now I'm with C#, having no experience how fast it will be in a certain real time surrounding.
PB is that fast...
Thanks again.
I will carefully check the new messages.
Heinz Grandjean

Edwin Knoppert

.NET is fast but it might be daunting choosing the best techniques.
This is imo where the most confusion is with .NET.

About my BSTR story, i was talking about the PB returns a BSTR and not where it expects a BSTR as parameter.
Thus (pseudocode):
Function test() Export as String
...

And in .NET intptr p = test()....

I am not aware of proper marshalling for this scenario (where .NET releases the BSTR handle).
I am sure there is something for this but never explored it, like i said i copied the BSTR data into something else using the garbage collector.

Hopefully this PB attempt is not something because i can do it faster in PB.
Forget .NET being slow, it's not! (it's very different though)

Mike Stefanik

Quote from: Heinz Grandjean on November 05, 2013, 05:27:32 PM
Following Patrice, I learned C++ only to realize that VS 2012 does no longer provide a GUI-toolkit for C++

MFC is still around, and there's both the App and Class Wizards which generates the framework code for a lot of different application types, and makes it easy to modify (map controls to class member variables, etc.)
Mike Stefanik
sockettools.com

Edwin Knoppert

Very old VB.NET code i ever used to move a BSTR handle into a byte array.
Using the Encoding classes you can convert to different things.


   Declare Ansi Sub SysFreeString Lib "OLEAUT32.DLL" Alias "SysFreeString" (ByVal bstr As Int32)
   Declare Ansi Function SysStringByteLen Lib "OLEAUT32.DLL" Alias "SysStringByteLen" (ByVal bstr As Int32) As Int32
   Declare Ansi Sub MoveMemory Lib "KERNEL32.DLL" Alias "RtlMoveMemory" (ByVal pDestination As Int32, ByVal pSource As Int32, ByVal cbLength As Int32)

  Public Function BSTRToByte(ByVal pBSTR As IntPtr) As Byte()
        If pBSTR = 0 Then Return Nothing
        Dim nLen As Int32 = SysStringByteLen(pBSTR)
        If nLen > 0 Then
            Dim b As Byte()
            ReDim b(nLen - 1)
            Dim GC As GCHandle = GCHandle.Alloc(b, GCHandleType.Pinned), pBuffer As IntPtr = GC.AddrOfPinnedObject.ToInt32() : GC.Free()
            If pBuffer <> 0 Then MoveMemory(pBuffer, pBSTR, nLen)
            SysFreeString(pBSTR)
            Return b
        End If
        SysFreeString(pBSTR)
        Return Nothing
    End Function


Please test since i rewrote it to this example actually..
And maybe some (API) changes to IntPtr would not harm i guess :)
This is for ANSI strings, you must rewrite it to unicode if needed.

Patrice Terrier

#12
Heinz

C/C++ is great to write small and fast 32/64-bit DLL(s), but to create interface fast, i always resort on something else  :-X

I liked the C# syntax, but i hate managed code and the extra overhead, and the huge framework forcing to wait several seconds (almost 20 seconds) the first time you start a .NET application.

The RAD i am using, starts immediatly, because it is written itself in C++, thus no need to load the huge .NET first.

Some (not me) seems to like QT, and there is also MFC...  :-[
Patrice Terrier
GDImage (advanced graphic addon)
http://www.zapsolution.com

Paul Elliott

Ok, I know it's crude and not commented well but here's some test code for transferring strings
between C# & PB.

Back in the early 2000's I was curious about calling TRM from a C# program so I wrote a C#
of one of my programs. It did the "Marshalling" thing all over the place. It worked but looked
very ungainly. So I continued working and finally got most everything working without the
Marshalling ( the early version of .Net had a flaw dealing with returning a string from a function).
That has been fixed in later versions of .Net runtime.

You can probably remove all the normal TRM.DLL code as the test program should only deal
with TRMPDE.DLL & TRMCNV.DLL.

As I said, it's crude but in my testing over the years it hasn't failed. But that may be just
because I didn't know what errors to test for.

The PB source was PB Win v9 & v10. The C# was tested with VS2005 & VS2010.

You are welcome to the code as is. If you find things wrong then please post any fixes so
that others may benefit.


source for trmpde.dll

' trmpde
' 12/24/2010 add seperate Unicode functions
' 12/25/2010 testing -- get rid of all extra code around strings
'            changing trm_StringU to trm_AsciiZ
'             with asciiz ptr as param

#COMPILE DLL "c:\windows\trmpde.dll"
#DIM ALL
#REGISTER NONE
#DEBUG ERROR ON

%USEMACROS = 1
#INCLUDE "Win32API.inc"

GLOBAL ghInstance AS DWORD

' The following User Defined Type (structured) variable is only used
' with the pointer-based API that passes parameters in the UDT...

TYPE TRMtype
    op      AS LONG  ' Tsunami operation number
    file    AS LONG  ' Tsunami file handle
    dataptr AS LONG  ' Address of data buffer
    datalen AS LONG  ' Length of data buffer
    keyptr  AS LONG  ' Address of key buffer
    keylen  AS LONG  ' Length of key buffer
    keyno   AS LONG  ' Key number
END TYPE

DECLARE FUNCTION trm_AsciiZ ALIAS "trm_AsciiZ" (BYVAL ASCIIZ PTR) AS STRING

DECLARE FUNCTION trm_String ALIAS "trm_String" ( STRING) AS STRING

DECLARE FUNCTION trm_Open ALIAS "trm_Open" ( STRING , LONG) AS LONG
' BYVAL ASCIIZ PTR, byval LONG ) AS LONG

DECLARE FUNCTION trm_Test ALIAS "trm_Test" ( BYVAL LONG, BYREF LONG, BYVAL LONG) AS STRING

DECLARE FUNCTION trm_Buffr ALIAS "trm_Buffr" ( BYVAL LONG, BYREF LONG, BYVAL LONG, BYREF LONG) AS LONG

DECLARE FUNCTION trm_udt ALIAS "trm_udt" ( BYREF x AS TRMtype ) AS LONG


'-------------------------------------------------------------------------------
' Main DLL entry point called by Windows...
'
FUNCTION LIBMAIN (BYVAL hInstance   AS LONG, _
                  BYVAL fwdReason   AS LONG, _
                  BYVAL lpvReserved AS LONG) AS LONG
    SELECT CASE fwdReason

    CASE %DLL_PROCESS_ATTACH
        'Indicates that the DLL is being loaded by another process (a DLL
        'or EXE is loading the DLL).  DLLs can use this opportunity to
        'initialize any instance or global data, such as arrays.

        ghInstance = hInstance
        OPEN "e:\trmopn.txt" FOR APPEND AS #1
        PRINT #1, ""
        PRINT #1, "----------"
        CLOSE #1

        FUNCTION = 1   'success!

        'FUNCTION = 0   'failure!  This will prevent the EXE from running.

    CASE %DLL_PROCESS_DETACH
        'Indicates that the DLL is being unloaded or detached from the
        'calling application.  DLLs can take this opportunity to clean
        'up all resources for all threads attached and known to the DLL.

        FUNCTION = 1   'success!

        'FUNCTION = 0   'failure!

    CASE %DLL_THREAD_ATTACH
        'Indicates that the DLL is being loaded by a new thread in the
        'calling application.  DLLs can use this opportunity to
        'initialize any thread local storage (TLS).

        FUNCTION = 1   'success!

        'FUNCTION = 0   'failure!

    CASE %DLL_THREAD_DETACH
        'Indicates that the thread is exiting cleanly.  If the DLL has
        'allocated any thread local storage, it should be released.

        FUNCTION = 1   'success!

        'FUNCTION = 0   'failure!

    END SELECT

END FUNCTION

' no conversion StrParm as ASCIIZ PTR
FUNCTION trm_AsciiZ(BYVAL zpStrParm AS ASCIIZ PTR ) EXPORT AS STRING
  LOCAL e AS LONG
  LOCAL adr AS LONG
  LOCAL str2 AS STRING
  LOCAL str1 AS STRING
  LOCAL str3 AS STRING
  LOCAL str AS STRING
  LOCAL asp AS ASCIIZ PTR
    OPEN "e:\trmopn.txt" FOR APPEND AS #1
    ERRCLEAR

    PRINT #1, ""
    PRINT #1, DATE$;"  "; TIME$
    PRINT #1, "trm_AsciiZ"
    str = @zpStrParm
    PRINT #1, "len zpStrParm ="; LEN(str)
    PRINT #1, "@zpStrParm ="; str
    e = ERR
    PRINT #1, "err = "; e
    CLOSE #1
    @zpStrParm = "back from PB" ' really shouldn't do this?
    str2 = str & "wowee"
    FUNCTION = str2
END FUNCTION

FUNCTION trm_String( StrParm AS STRING ) EXPORT AS STRING
  LOCAL e AS LONG
  LOCAL adr AS LONG
  LOCAL str2 AS STRING
  LOCAL str AS STRING
  LOCAL asp AS ASCIIZ PTR
    OPEN "e:\trmopn.txt" FOR APPEND AS #1
    ERRCLEAR

    PRINT #1, ""
    PRINT #1, DATE$;"  "; TIME$
    PRINT #1, "trm_String"
    PRINT #1, "len StrParm ="; LEN(StrParm)
    IF LEN(StrParm) < 50 THEN
        PRINT #1, "StrParm ="; StrParm
    ELSE
        PRINT #1, "* StrParm ="; LEFT$(StrParm, 12)
    END IF

    asp = STRPTR(StrParm)
    str = @asp
    PRINT #1, "from Asciiz Ptr ="; str

    e = ERR
    PRINT #1, "err = "; e
    CLOSE #1
    StrParm = "wowbag.txt"
    str2 = str & "wowee"
    FUNCTION = str2
END FUNCTION

FUNCTION trm_Buffr( BYVAL bptr AS LONG, BYREF sizbbufr AS LONG, BYVAL cptr AS LONG, BYREF sizcbufr AS LONG) EXPORT AS LONG
  LOCAL x AS LONG
  LOCAL y AS LONG
  STATIC str AS STRING
  LOCAL str2 AS STRING
  LOCAL str3 AS STRING
  LOCAL z AS LONG
    x = sizbbufr
    y = sizcbufr
    str = "PB string " & CHR$(0) & "<0" & STR$(x + y) + CHR$(255)

    str2 = PEEK$(bptr, x)
    str3 = PEEK$(cptr, y)
    OPEN "e:\trmopn.txt" FOR APPEND AS #1
    PRINT #1, ""
    PRINT #1, DATE$;"  "; TIME$
    PRINT #1, "in trm_Buffr"
    PRINT #1, "len bbufr in ="; x
    PRINT #1, "bbufr in ="; str2;"<<"
    PRINT #1, "len cbufr in ="; y
    PRINT #1, "cbufr in ="; str3;"<<"
    CLOSE #1

    z = STRPTR(str)
    ' dest src len
    MoveMemory BYVAL bptr, BYVAL z, LEN(str)
    MoveMemory BYVAL cptr, BYVAL z, LEN(str)
    sizbbufr = LEN(str)
    sizcbufr = LEN(str)
    FUNCTION = LEN(str)
END FUNCTION

FUNCTION trm_Test( BYVAL hndl AS LONG, BYREF rtc AS LONG, BYVAL bptr AS LONG) EXPORT AS STRING
  LOCAL x AS LONG
  LOCAL y AS LONG
  STATIC str AS STRING
  LOCAL str2 AS STRING
  LOCAL z AS LONG
    x = rtc
    str = "PB string " & CHR$(0) & "<0" & STR$(hndl + x) + CHR$(255)

    str2 = PEEK$(bptr, x)
    OPEN "e:\trmopn.txt" FOR APPEND AS #1
    PRINT #1, ""
    PRINT #1, DATE$;"  "; TIME$
    PRINT #1, "in trm_Test"
    PRINT #1, "len bufr in ="; x
    PRINT #1, "bufr in ="; str2;"<<"
    CLOSE #1

    z = STRPTR(str)
    ' dest src len
    MoveMemory BYVAL bptr, BYVAL z, LEN(str)
    rtc = LEN(str)
    FUNCTION = str
END FUNCTION

FUNCTION trm_Open( PathFileName AS STRING, multi AS LONG ) EXPORT AS LONG
  LOCAL e AS LONG
  LOCAL adr AS LONG
  LOCAL str2 AS STRING
  LOCAL str AS STRING
  LOCAL pbstr AS STRING
  LOCAL asp AS ASCIIZ PTR

    ' MSGBOX "waiting"
    OPEN "e:\trmopn.txt" FOR APPEND AS #1
    ERRCLEAR
   ' ON ERROR GOTO bad
    PRINT #1, ""
    PRINT #1, DATE$;"  "; TIME$
    PRINT #1, "trm_Open"
    PRINT #1, "len PathFileName ="; LEN(PathFileName)
    IF LEN(PathFileName) < 50 THEN
        str = PathFileName
        PRINT #1, "PathFileName ="; str
    END IF
    adr = STRPTR(PathFileName) ' use strptr for string
    str2 = PEEK$(adr, 10)
    PRINT #1, "strptr adr PathFileName ="; adr; "  hex ="; HEX$(adr)
    PRINT #1, "content adr ="; str2;"<<"
    IF MID$(PathFileName, 2, 1) = CHR$(0) THEN
        str = ACODE$(PathFileName)
        PRINT #1, "ACODE len PathFileName ="; LEN(str)
        PRINT #1, "ACODE PathFileName ="; str
    END IF
    asp = STRPTR(PathFileName) ' use strptr for string
    str = @asp
    PRINT #1, "from Asciiz Ptr ="; str

    e = ERR
    PRINT #1, "err = "; e
    PRINT #1, "multi ="; multi
    CLOSE #1
#IF 1
    IF e = 0 THEN
        FUNCTION = multi + LEN(PathFileName)
        IF LEN(PathFileName) > 3 THEN
            MID$(PathFileName, 1, 3) = "wow"
        ELSE
            PathFileName = "wowbag.txt"
        END IF
    ELSE
        FUNCTION = e
    END IF
#ELSE
    FUNCTION = e
#ENDIF
    EXIT FUNCTION

bad:
    e = ERR
    ON ERROR GOTO 0
    PRINT #1, ""
    PRINT #1, "err ="; e
    CLOSE #1
    FUNCTION = 101
END FUNCTION

FUNCTION trm_udt( BYREF x AS TRMtype ) EXPORT AS LONG
  LOCAL e AS LONG
  LOCAL el AS LONG
  LOCAL str AS STRING
  LOCAL bak AS STRING
  LOCAL z AS LONG
    OPEN "c:\trmopn.txt" FOR APPEND AS #1
    ON ERROR GOTO bad
    PRINT #1, ""
    PRINT #1, "we're here UDT "; DATE$; "  "; TIME$
3   PRINT #1, "x.op ="; x.op
#IF 1
4   PRINT #1, "x.keyno ="; x.keyno
5   IF x.keylen > 0 THEN
7   str = SPACE$(x.keylen)
9   str = PEEK$(x.keyptr, x.keylen)
    'z = STRPTR(str)
    'MoveMemory BYVAL z, BYVAL x.dataptr, x.datalen
    PRINT #1, "x.key ="; str
    PRINT #1, "x.keyptr ="; x.keyptr
    PRINT #1, "x.len key ="; x.keylen
    ELSE
        PRINT #1, "no key info"
    END IF
21  IF x.datalen > 0 THEN
23  str = SPACE$(x.datalen)
25  str = PEEK$(x.dataptr, x.datalen)
    'z = STRPTR(str)
    'MoveMemory BYVAL z, BYVAL x.dataptr, x.datalen
    PRINT #1, "x.data ="; str
    PRINT #1, "x.dataptr ="; x.dataptr
    PRINT #1, "x.len data ="; x.datalen
    ELSE
        PRINT #1, "no data info"
    END IF
#ENDIF
    PRINT #1, "--------------------------------------------"
    CLOSE #1

    'bak = STRREVERSE$(str)
    'POKE$ x.dataptr, bak
    x.file = 1
    FUNCTION = 1
    EXIT FUNCTION

bad:
    e = ERR
    'el = ERL
    ON ERROR GOTO 0
    PRINT #1, ""
    PRINT #1, "err ="; ERR; " at ln#"; el
    CLOSE #1
    x.file = e
    FUNCTION = 101
END FUNCTION



source for trmcnv.dll

' TRMCNV
' 05/28/2013
'
' interface between C# & Tsunami

#Compile DLL "C:\Windows\TRMCNV.DLL"
#Dim All
#Register None

#Include Once "Win32API.inc"
#Include Once "D:\PB\Tsunami\TRM.inc"

'// Declare Function trmOpenN Alias "trmOpenN" ( String, Long) As Long 06/01/2013 does not work
Declare Function trmOpenN Alias "trmOpenN" ( ByVal Byte Ptr, Long) As Long
Declare Function trmOpen Alias "trmOpen" ( ByVal Long, ByVal Byte Ptr, ByVal Long) As Long
Declare Function trmInsert Alias "trmInsert" ( ByVal Long, ByVal Long, ByVal Byte Ptr) As Long

Global ghInstance As Dword


'-------------------------------------------------------------------------------
' Main DLL entry point called by Windows...
'
Function LibMain (ByVal hInstance   As Long, _
                  ByVal fwdReason   As Long, _
                  ByVal lpvReserved As Long) As Long

    Select Case fwdReason

    Case %DLL_PROCESS_ATTACH
        'Indicates that the DLL is being loaded by another process (a DLL
        'or EXE is loading the DLL).  DLLs can use this opportunity to
        'initialize any instance or global data, such as arrays.

        ghInstance = hInstance

        Function = 1   'success!

        'FUNCTION = 0   'failure!  This will prevent the EXE from running.

    Case %DLL_PROCESS_DETACH
        'Indicates that the DLL is being unloaded or detached from the
        'calling application.  DLLs can take this opportunity to clean
        'up all resources for all threads attached and known to the DLL.

        Function = 1   'success!

        'FUNCTION = 0   'failure!

    Case %DLL_THREAD_ATTACH
        'Indicates that the DLL is being loaded by a new thread in the
        'calling application.  DLLs can use this opportunity to
        'initialize any thread local storage (TLS).

        Function = 1   'success!

        'FUNCTION = 0   'failure!

    Case %DLL_THREAD_DETACH
        'Indicates that the thread is exiting cleanly.  If the DLL has
        'allocated any thread local storage, it should be released.

        Function = 1   'success!

        'FUNCTION = 0   'failure!

    End Select

End Function

Function trmOpenN( ByVal uFileName As Byte Ptr, mu As Long) Export As  Long
   Local aFileName As String
   Local rtc As Long
   Local x As Long

   Open "E:\CSdump.txt" For Output As #1
   ' MsgBox "waiting len = " & Format$(Len(aFileName))
   aFileName = Space$(100)
   For x = 1 To 30
      Mid$(aFileName, x, 1) = Chr$(@uFileName)
      Print #1, Str$(x);" >"; Format$(@uFileName); "< >"; Chr$(@uFileName)
      Incr uFileName
   Next
   Close #1
   MsgBox "aFileName = >" & aFileName & "<"

   ' rtc = trm_Open(aFileName, mu)

   Function = rtc
End Function

Function trmOpen( ByVal fnLen As Long, ByVal uFileName As Byte Ptr, ByVal mu As Long) Export As  Long
   Local aFileName As String
   Local rtc As Long
   Local x As Long

   'MsgBox "waiting len = " & Format$(fnLen)
   aFileName = Space$(fnLen)
   For x = 1 To fnLen
      Mid$(aFileName, x, 1) = Chr$(@uFileName)
      Incr uFileName
   Next
   'MsgBox "aFileName = >" & aFileName & "<"

   rtc = trm_Open(aFileName, mu)

   Function = rtc
End Function

Function trmInsert( ByVal FileHandle As Long, ByVal uRecordLen As Long, ByVal uRecord As Byte Ptr) Export As  Long
   Local aRecord As String
   Local rtc As Long
   Local x As Long

   aRecord = Space$(uRecordLen)
   For x = 1 To uRecordLen
      Mid$(aRecord, x, 1) = Chr$(@uRecord)
      Incr uRecord
   Next
   rtc = trm_Insert(FileHandle, aRecord)
   Function = rtc
End Function



source for form1.cs

// trmtest1
// 12/13/2010
// 12/13/2010 got very simple fix for trm_Open string to work
// 12/14/2010 working on trm_String
//            put stuff in listbox
// 12/24/2010 trying a plain Unicode set of strings
// 12/25/2010 testing -- get rid of all extra around strings
//             changing trm_StringU to trm_AsciiZ ** don't change zpStrParam **
// 05/28/2013 add TRMCNV.dll to handle Unicode<-->ASCII strings

using System;
using System.Drawing;
using System.Collections;
using System.ComponentModel;
using System.Windows.Forms;
using System.Data;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;

namespace trmtest1
{
    /// <summary>
    /// Summary description for Form1.
    /// </summary>
    public class Form1 : System.Windows.Forms.Form
    {
        private System.Windows.Forms.Button button1;
        private System.Windows.Forms.ListBox listBox1;
        private System.Windows.Forms.Label label1;
        private System.Windows.Forms.Label label2;
        private System.Windows.Forms.Label label3;
        private System.Windows.Forms.Label label4;
        private Label label5;
        private Button button2;
        private Button button3;
        private TextBox textBox1;
        /// <summary>
        /// Required designer variable.
        /// </summary>
        private System.ComponentModel.Container components = null;

        // 05/26/2013 going with real TRM UNLESS a string is a parameter!!
        // trm_Close (1)
        [DllImport("TRM.DLL")]
        public static extern int trm_Close(ref int FileHandle);
        [DllImport("TRM.DLL")]
        public static extern int trm_Count(ref int FileHandle);
        [DllImport("TRM.DLL")]
        public static extern int trm_Insert(ref int FileHandle, ref string record);
        [DllImport("TRMCNV.DLL")]
        public static extern int trmInsert(int FileHandle, int recLen, byte[] bufr);
        [DllImport("TRM.DLL")]
        public static extern string trm_GetLast(ref int FileHandle, ref int kee);
        [DllImport("TRM.DLL")]
        public static extern int trm_Result(ref int FileHandle);

        // trm_Open (0)
        [DllImport("TRMCNV.DLL")]
        public static extern int trmOpenN(ref string filename, ref int mu);
        [DllImport("TRMCNV.DLL")]
        public static extern int trmOpen(int fnLen, byte[] bufr, int mu);
        // 05/26/2013 going with real TRM
       

        // trm_Test (String-based API Only)
        [DllImport("TRMPDE.DLL")]
        // unsafe public static extern string trm_Test(int FileHandle, int * dummy, byte * work);
        unsafe public static extern IntPtr trm_Test(int FileHandle, int* dummy, byte* work);

        // trm_String string in/out as param & returns string
        [DllImport("TRMPDE.DLL")]
        [return: MarshalAs(UnmanagedType.AnsiBStr)]
        public static extern string trm_String(
           [MarshalAs(UnmanagedType.AnsiBStr)] ref string work);

        // trm_AsciiZ asciiz ptr in as param & returns string
        [DllImport("TRMPDE.DLL")]
        public static extern string trm_AsciiZ(string work);

        public Form1()
        {
            //
            // Required for Windows Form Designer support
            //
            InitializeComponent();

            //
            // TODO: Add any constructor code after InitializeComponent call
            //
        }

        /// <summary>
        /// Clean up any resources being used.
        /// </summary>
        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (components != null)
                {
                    components.Dispose();
                }
            }
            base.Dispose(disposing);
        }

        #region Windows Form Designer generated code
        /// <summary>
        /// Required method for Designer support - do not modify
        /// the contents of this method with the code editor.
        /// </summary>
        private void InitializeComponent()
        {
            this.button1 = new System.Windows.Forms.Button();
            this.listBox1 = new System.Windows.Forms.ListBox();
            this.label1 = new System.Windows.Forms.Label();
            this.label2 = new System.Windows.Forms.Label();
            this.label3 = new System.Windows.Forms.Label();
            this.label4 = new System.Windows.Forms.Label();
            this.label5 = new System.Windows.Forms.Label();
            this.button2 = new System.Windows.Forms.Button();
            this.button3 = new System.Windows.Forms.Button();
            this.textBox1 = new System.Windows.Forms.TextBox();
            this.SuspendLayout();
            //
            // button1
            //
            this.button1.Location = new System.Drawing.Point(363, 13);
            this.button1.Name = "button1";
            this.button1.Size = new System.Drawing.Size(48, 24);
            this.button1.TabIndex = 0;
            this.button1.Text = "Start";
            this.button1.Click += new System.EventHandler(this.button1_Click);
            //
            // listBox1
            //
            this.listBox1.Location = new System.Drawing.Point(8, 48);
            this.listBox1.Name = "listBox1";
            this.listBox1.Size = new System.Drawing.Size(680, 186);
            this.listBox1.TabIndex = 1;
            //
            // label1
            //
            this.label1.Location = new System.Drawing.Point(16, 248);
            this.label1.Name = "label1";
            this.label1.Size = new System.Drawing.Size(296, 24);
            this.label1.TabIndex = 2;
            this.label1.Text = "label1";
            //
            // label2
            //
            this.label2.Location = new System.Drawing.Point(360, 248);
            this.label2.Name = "label2";
            this.label2.Size = new System.Drawing.Size(320, 16);
            this.label2.TabIndex = 3;
            this.label2.Text = "label2";
            //
            // label3
            //
            this.label3.Location = new System.Drawing.Point(8, 280);
            this.label3.Name = "label3";
            this.label3.Size = new System.Drawing.Size(672, 24);
            this.label3.TabIndex = 4;
            this.label3.Text = "label3";
            //
            // label4
            //
            this.label4.Location = new System.Drawing.Point(8, 320);
            this.label4.Name = "label4";
            this.label4.Size = new System.Drawing.Size(664, 24);
            this.label4.TabIndex = 5;
            this.label4.Text = "label4";
            //
            // label5
            //
            this.label5.Location = new System.Drawing.Point(5, 348);
            this.label5.Name = "label5";
            this.label5.Size = new System.Drawing.Size(664, 24);
            this.label5.TabIndex = 6;
            this.label5.Text = "label5";
            //
            // button2
            //
            this.button2.Location = new System.Drawing.Point(543, 12);
            this.button2.Name = "button2";
            this.button2.Size = new System.Drawing.Size(81, 23);
            this.button2.TabIndex = 7;
            this.button2.Text = "trm_String";
            this.button2.UseVisualStyleBackColor = true;
            this.button2.Click += new System.EventHandler(this.button2_Click);
            //
            // button3
            //
            this.button3.Location = new System.Drawing.Point(435, 13);
            this.button3.Name = "button3";
            this.button3.Size = new System.Drawing.Size(89, 22);
            this.button3.TabIndex = 8;
            this.button3.Text = "AsciiZ";
            this.button3.UseVisualStyleBackColor = true;
            this.button3.Click += new System.EventHandler(this.button3_Click);
            //
            // textBox1
            //
            this.textBox1.Location = new System.Drawing.Point(19, 15);
            this.textBox1.Name = "textBox1";
            this.textBox1.Size = new System.Drawing.Size(273, 20);
            this.textBox1.TabIndex = 9;
            this.textBox1.Text = "E:\\CStest.trm";
            //
            // Form1
            //
            this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);
            this.ClientSize = new System.Drawing.Size(704, 381);
            this.Controls.Add(this.textBox1);
            this.Controls.Add(this.button3);
            this.Controls.Add(this.button2);
            this.Controls.Add(this.label5);
            this.Controls.Add(this.label4);
            this.Controls.Add(this.label3);
            this.Controls.Add(this.label2);
            this.Controls.Add(this.label1);
            this.Controls.Add(this.listBox1);
            this.Controls.Add(this.button1);
            this.Name = "Form1";
            this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
            this.Text = "Form1";
            this.ResumeLayout(false);
            this.PerformLayout();

        }
        #endregion

        /// <summary>
        /// The main entry point for the application.
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.Run(new Form1());
        }

        private void button1_Click(object sender, System.EventArgs e)
        {
            int rtc;
            String filename = String.Empty;
            String aFilename;
            int mu;
            int hndl;
            String back = String.Empty;
            String work = String.Empty;
            int lb;
            byte[] bufr = new byte[512];
            IntPtr pbStr;
            int lbidx;
            IntPtr backPtr;
            int recs;
            int kee;
            int x;
           
            rtc = 0;
            filename = textBox1.Text;
            if (filename.Length < 1) {
                filename = @"E:\CStest.trm";
                textBox1.Text = filename;
            }
            mu = 0;
            rtc = 0;
            hndl = 0;
            back = "";
            lb = 0;
            recs = 0;
            kee = 1;

            lbidx = listBox1.Items.Count;
            listBox1.Items.Insert(lbidx++, "hndl = trmOpen(fnlen, bufr, mu);");
           
            //for (x = 0; x < filename.Length; x++)
            //{
            //    bufr[x] = (byte)filename[x];
            //}
            bufr = Encoding.ASCII.GetBytes(filename);
            //bufr[filename.Length] = 0;

            hndl = trmOpen(filename.Length, bufr, mu);
           
           
            bufr = Encoding.ASCII.GetBytes(filename);
            // bufr[filename.Length] = 0;
            aFilename = Encoding.ASCII.GetString(bufr);
           // hndl = trmOpenN(ref aFilename, ref mu);
           

            label1.Text = hndl.ToString() + " open hndl";

            // hndl = rtc;
            if (hndl > 0)
            {
                recs = trm_Count(ref hndl);
                if (recs > 0)
                {
                    work = trm_GetLast(ref hndl, ref kee);
                    rtc = trm_Result(ref hndl);
                    label3.Text = "trm_GetLast rtc = " + rtc.ToString() + "  >>" + work + "<<";
                    listBox1.Items.Insert(lbidx++, label3.Text);
                }
                recs++;
                work = recs.ToString() + " the real data comes around here"; // + " " + DateTime.Now.ToString()
                label5.Text = work;
                //for (x = 0; x < work.Length; x++)
                //{
                //    bufr[x] = (byte)work[x];
                //}
                bufr = Encoding.ASCII.GetBytes(work);
                //bufr[work.Length] = 0;
                rtc = trmInsert(hndl, work.Length, bufr);
                label4.Text = "trmInsert rtc = " + rtc.ToString();
                listBox1.Items.Insert(lbidx++, label4.Text);
                rtc = trm_Close(ref hndl);
                label2.Text = rtc.ToString() + " close rtc";
            }

        }


        private void button2_Click(object sender, EventArgs e)
        {
            string StrParm;
            string StrBack;
            // IntPtr backptr;
            // int backlen;

            StreamWriter txtOut;
            string pth;
            string wrk;
            int lbidx;

            lbidx = listBox1.Items.Count;
            //if (lbidx > 0)  lbidx++;
            listBox1.Items.Insert(lbidx++, "StrBack = trm_String(ref StrParm);");
            StrParm = "paul.txt\0wow";
            //backptr = trm_String(ref StrParm);
            //backlen = SysStringByteLen(backptr);
            StrBack = "**";

            StrBack = trm_String(ref StrParm);
            label2.Text = "StrParm =" + StrParm + "<<";
            //StrBack = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(backptr, backlen);
            label3.Text = "StrBack =" + StrBack + "<< len =" + StrBack.Length.ToString();

            pth = @"E:\trmopn.txt";
            txtOut = File.AppendText(pth);
            wrk = "StrBack len =" + StrBack.Length.ToString() + " >>" + StrBack + "<<";
            txtOut.WriteLine(wrk);
            listBox1.Items.Insert(lbidx++, wrk);
            wrk = "StrParm len =" + StrParm.Length.ToString() + " >>" + StrParm + "<<";
            txtOut.WriteLine(wrk);
            listBox1.Items.Insert(lbidx++, wrk);

            txtOut.Close();
        }

        private void button3_Click(object sender, EventArgs e)
        {
            string StrParm;
            string StrBack;

            StreamWriter txtOut;
            string pth;
            string wrk;
            int lbidx;

            lbidx = listBox1.Items.Count;
            listBox1.Items.Insert(lbidx++, "StrBack = trm_AsciiZ( string StrParm);");
            StrParm = "paul.txt wow";
            StrBack = "**";

            StrBack = trm_AsciiZ(StrParm);

            label2.Text = "StrParm =" + StrParm + "<<";
            label3.Text = "StrBack =" + StrBack + "<< len =" + StrBack.Length.ToString();

            pth = @"E:\trmopn.txt";
            txtOut = File.AppendText(pth);
            wrk = "StrBack len =" + StrBack.Length.ToString() + " >>" + StrBack + "<<";
            txtOut.WriteLine(wrk);
            listBox1.Items.Insert(lbidx++, wrk);
            wrk = "StrParm len =" + StrParm.Length.ToString() + " >>" + StrParm + "<<";
            txtOut.WriteLine(wrk);
            listBox1.Items.Insert(lbidx++, wrk);

            txtOut.Close();

        }
    }
}


        /*
         * UnicodeToAnsi converts the Unicode string pszW to an ANSI string
         * and returns the ANSI string through ppszA. Space for the
         * the converted string is allocated by UnicodeToAnsi.
         */
/*
        public int UnicodeToAnsi(LPCOLESTR pszW, LPSTR* ppszA)
        {

            ULONG cbAnsi, cCharacters;
            DWORD dwError;

            // If input is null then just return the same.
            if (pszW == NULL)
            {
                *ppszA = NULL;
                return NOERROR;
            }

            cCharacters = wcslen(pszW) + 1;
            // Determine number of bytes to be allocated for ANSI string. An
            // ANSI string can have at most 2 bytes per character (for Double
            // Byte Character Strings.)
            cbAnsi = cCharacters * 2;

            // Use of the OLE allocator is not required because the resultant
            // ANSI  string will never be passed to another COM component. You
            // can use your own allocator.
            *ppszA = (LPSTR)CoTaskMemAlloc(cbAnsi);
            if (NULL == *ppszA)
                return E_OUTOFMEMORY;

            // Convert to ANSI.
            if (0 == WideCharToMultiByte(CP_ACP, 0, pszW, cCharacters, *ppszA,
                          cbAnsi, NULL, NULL))
            {
                dwError = GetLastError();
                CoTaskMemFree(*ppszA);
                *ppszA = NULL;
                return HRESULT_FROM_WIN32(dwError);
            }
            return NOERROR;

        }
  */


/*  05/26/2013 trying real TRM open / close
           listBox1.Items.Insert(lbidx++, "backPtr = trm_Test(hndl, &rtc, bptr);");
           unsafe {
               fixed  ( byte * bptr = bufr) {
                   for ( lb = 0; lb < filename.Length; lb++ ) {
                       bufr[lb] = (byte) filename[lb];
                   }
                   rtc = filename.Length;
                   // back = trm_Test(hndl, &rtc, bptr);
                   // 11/13/2006 1pm -- able to retrieve ALL of returned string
                   //   but need length from somewhere
                   listBox1.Items.Insert(lbidx++, "filename b4 Test >" + filename);
                   backPtr = trm_Test(hndl, &rtc, bptr);
                   bufr[10] = (byte) 'x';
                   work = System.Runtime.InteropServices.Marshal.PtrToStringAnsi((IntPtr)bptr, rtc);
                   back = System.Runtime.InteropServices.Marshal.PtrToStringAnsi(backPtr, rtc);
               }
           }
           
           lb = back.Length;
           label3.Text = "Test back len =" + lb.ToString() + ">>" + back + "<< ";
           
           lb = work.Length;
           label4.Text = "Test work len =" + lb.ToString() + " >>" + work + "<<";
           
           listBox1.Items.Insert(lbidx++, label3.Text);
           listBox1.Items.Insert(lbidx++, label4.Text);
           
           pbStr = System.Runtime.InteropServices.Marshal.StringToBSTR(filename);
           listBox1.Items.Insert(lbidx++, "rtc = trm_Open(ref filename,  ref mu);");
           unsafe {
               void * xpbStr;
               xpbStr = &pbStr;
               // rtc = trm_Open(pbStr, &mu);
               listBox1.Items.Insert(lbidx++, "filename b4 Open >" + filename);
               rtc = trm_Open(ref filename,  ref mu);
               label5.Text = "filename from _Open >" + filename;
           }
           
           listBox1.Items.Insert(lbidx++, label5.Text);
           System.Runtime.InteropServices.Marshal.FreeBSTR(pbStr);
            */


Heinz Grandjean

Paul,
I was not able to test the whole application because the BP-DLL searches for an Inc. I could not find.
But I got information about the way you handle the unsafe calls.

C#-Code:

         //getting a marshalled string as return-value from a PB_DLL:
         //==========================================================
        [DllImport(dll1, EntryPoint = "testme3")]
        [return: MarshalAs(UnmanagedType.AnsiBStr)]
        public static extern string NetTest3([MarshalAs(UnmanagedType.AnsiBStr)] ref string mytext2);

           string mytext2 = "String as retVal";
           string mytextReturn = NetTest3(ref mytext2);
           Console.WriteLine();
           Console.WriteLine(mytextReturn);



PB_Code:

FUNCTION  NetTest3 ALIAS "testme3"( _
                                  BYREF strText AS STRING _
                                  ) STATIC EXPORT AS STRING
       ? strText
         strText += " has been processed!"
       ? strText
       FUNCTION = strText
END FUNCTION     


This call works perfect.

The next example crashes in C# after C# returns from the call,
if the marshalling command is missing:
C#-Code:

         //getting a non-marshalled string as return-value from a PB_DLL:
         //=============================================================
         [DllImport(dll1, EntryPoint = "testme_ASC")]
         [return: MarshalAs(UnmanagedType.AnsiBStr)] // without this it crashes!!!
          public static extern string NetTest_Asc(string mytext3);   
           string mytext3 = "Non-marshalled String as retVal";
           string mytextReturn2;

           mytextReturn2 = NetTest_Asc(mytext3);
           Console.WriteLine();
           Console.WriteLine(mytextReturn2);


PB-Code:

FUNCTION  NetTest_Asc ALIAS "testme_ASC"( _
                                        BYVAL zpStrText AS ASCIIZ PTR _
                                        ) STATIC EXPORT AS STRING
        LOCAL strText AS STRING
        strText = @zpStrText
       ? strText
         strText += " has been processed!"
       ? strText
       FUNCTION = @zpStrText
END FUNCTION         

Inside the PB-DLL I can display the ASCIIZ PTR, but C# crashes after returning.

Thanks, Paul

Heinz Grandjean