Forum Discussion

rsippel's avatar
rsippel
Occasional Contributor
14 years ago

Calling functions in a DLL from an extension


I'm using TC8.1, have created an extension, and am now trying to call functions in a DLL from the extension code.


Initially we were adding the DLL to the CLRBridge of our main project (not the extension) and were using the dotNET object to call the functions in the dll. That seemed to work.


Now we are using extensions and need to call the DLL from the extension code too. Actually, since some of the extension code needs to call the DLL, I've moved everything that needs the DLL into the extension. Now I am having problems calling the DLL functions.


I originally tried adding the DLL to the extension's CLRBridge, and it did seem to work...sort of. It does not make sense to me that it should work at all since the tcx file for the extension only includes code files and the description.xml, so where would the CLRBridge info be included?


I did read that there is a way to go around the CLRBridge by using AppDomain. However, it is not well documented (at least as far as whether it can be used with DLL's as opposed to exe's)...so I gave up trying that.


I found another set of docs about calling functions in DLL's. It's complicated. You have to even prototype the functions that you want to call in the DLL, and you have to be careful about clearly specifying the types of parameters being passed. It seems to be like setting up the COM connections back in C++. I seem to have gotten through it but when I actually call a DLL function I get an exception "Object doesn't support this property or method."


It does not matter whether I call a function that has no parameters or a function that has parameters.


 


function Test_Misc()


{


var function_name = "reportservice_util_unit_test.Test_Misc";


var dll_path_name = "D:\\sandbox-v3-CopyLog\\Utilities\\LogGenerator.dll";


var dll_nickname = "LogGenerator";


var dll_environment = null; // Def_Environment in docs.


var dll_type = null; // Def_DLL in docs.


var dll_object = null; // Lib in docs.


try


{


//##########################################################################


// Define an environment (32-bit or 64-bit) for loading the DLL into.


// In this case it should go into the 32-bit environment automatically since the DLL is 32-bit.


//##########################################################################


dll_environment = DLL.DefineEnvironmentByDLL( dll_path_name );


if( dll_environment == null )


{


HelperForUnitTesting_TestFailed( function_name, "Unable to create an DLL Environment for [" + dll_path_name + "]." );


return false;


}


//##########################################################################


// Get a helper object to use in defining the type of routines in the DLL.


//##########################################################################


dll_type = dll_environment.DefineDLL( dll_nickname );


if( dll_type == null )


{


HelperForUnitTesting_TestFailed( function_name, "Unable to create an DLL Type for [" + dll_path_name + "]." );


return false;


}


//##########################################################################


// Define the functions to expect in the DLL.


// NOTE that first parameter is the FUNCTION_NAME.


// NOTE that final parameter is the FUNCTION_RETURN_TYPE (use vt_empty or vt_void if no return value).


//##########################################################################


// No parameters.


dll_type.DefineProc( "AppendEmptyLog", vt_void );


// Parameter1: ProjectName


// Parameter2: Machinename


dll_type.DefineProc( "CreateDateFolder", vt_lpstr, vt_lpstr, vt_void );


//##########################################################################


//##########################################################################


dll_object = dll_environment.Load( dll_path_name, dll_nickname );


if( dll_object == null )


{


HelperForUnitTesting_TestFailed( function_name, "Unable to create an DLL Object for [" + dll_path_name + "]." );


return false;


}


// Try a routine that has no parameters.


dll_object.AppendEmptyLog();


// Try a routine that has parameters.


var project_name = dll_environment.New( "LPSTR", 256 );


var machine_name = dll_environment.New( "LPSTR", 256 );


project_name.Text = "TestingDLL";


machine_name.Text = "ABCD";


dll_object.CreateDateFolder( project_name, machine_name );


}


catch( exception )


{


HelperForUnitTesting_TestFailed( function_name, Messages_CreateMessageForException( exception ) );


return false;


}


HelperForUnitTesting_TestSucceeded( function_name );


return true;


}


I get the exception whether I call AppendEmptyLog() or CreateDateFolder().


Any help would be appreciated. Thanks.


6 Replies

  • Hello Rob,


    Do you call the presented script code from a script extension? Please note that the DLL object you use in the code is not supported by the script extension engine. You can see the list of objects available in script extensions here.


    Unfortunately, the script extension engine does not provide any straightforward way to call DLL routines. It is not designed for this purpose.

    You can still call DLL routines via the dotNet object in your extensions as you used to. However, as the extension files cannot store any CRL Bridge information indeed, you may have to manually add the target DLL reference to the CLR Bridge list of any TestComplete project where you are planning to run your extension.

    As for the AppDomain method, it is applied only to process objects. You can use it in your script extensions, but you need to create an application that has a reference to your DLL.

  • rsippel's avatar
    rsippel
    Occasional Contributor
    Thanks Julia.



    That clears things up a lot.



    My Test_Misc() function was in my extension and was my attempt to call functions in the LogGenerator.dll.



    I was hoping to be able to have only the extension need to know about the LogGenerator.dll. I wanted to free the projects that use the extension from having to know anything about the dll. It looks like there is a limitation in extensions that will not let me do that (since it sounds like I have to add LogGenerator.dll to the CLRBridge for each of my projects in order for the extension to be able to use dotNET to call the dll).



    That is what we were originally doing. However, that is forcing the writers of the projects to have to remember to add LogGenerator.dll to the CLRBridge of the project. We also seem to have problems getting the calls to LogGenerator.dll to work consistently. Sometimes reloading in CLRBridge helps and sometimes we have to remove and re-add LogGenerator.dll to the CLRBridge. This seems to be a lot of maintenance and it does not always fix the problem (I seem to get "[object Error] : Exception: [null]." a lot when I try to call LogGenerator.dll functions).



    Maybe we'll try creating an exe that wraps the calls to the dll, as you suggested.



    ~Rob
  • rsippel's avatar
    rsippel
    Occasional Contributor
    Well, I tried copying the function out of the extension and into my test-suite to see if the DLL.DefineEnvironmentByDLL()...Load()...dll_object.function() logic would work outside of the extension. I ended up getting to the point of trying to call the function and then got the exception "Object doesn't support this property or method".



    I did a google search on DefineEnvironmentByDLL and found someone else who indicated that they were having problems with this stuff in TC8 (though it had been working for them in TC7).



    http://www.sqaforums.com/showflat.php?Cat=&Board=UBB43&Number=639128&Searchpage=1&Main=638878&Words=+Allen_AQA&topic=&Search=true



    I am using TC8.10.487.7.

    The link did not provide a solution (they took the issue out to email so that's no help to anyone else).



    Anyone have suggestions?



    I also looked into trying to use the AppDomain() approach, which needs an EXE to wrap calls to the DLL. There are no examples about how to do that. What should the EXE code look like? Which template should be used for it? How does one start the EXE from an extension?



    The dotNET approach does work, but we have a lot of test-projects and really would like it if we did not have to maintain the CLRBridge in each of them just so the extension will work (all of the DLL calls are in the extension).



    Thanks for any consideration.

    ~Rob
  • Hello Rob,


    Please make sure that the routines in your DLL match the stdcall calling convention. If this does not help, please send our Support Team a sample DLL demonstrating the problem. This would help us analyze and reproduce the problem.


    Also, though the script extension engine does not currently support working with DLLs via the DLL object, we have a corresponding suggestion registered in our database, and your interest in this issue has increased its rating.




    I also looked into trying to use the AppDomain() approach, which needs an EXE to wrap calls to the DLL. There are no examples about how to do that. What should the EXE code look like? Which template should be used for it?


    To use the AppDomain approach, you need to create an executable that loads your DLL in the application domain. For example, if you use C#, consider the following sample code:



    using System.Reflection;

    namespace Sample

    {

      public void Main()

      {

        Assembly SampleAssembly = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + "Sample.dll");

      }

    }



    For more information on loading DLLs, please refer to the MSDN Library.




    How does one start the EXE from an extension?


    Since the script extension engine does not provide any straightforward way to start an executable, you can use the following workaround:



    AppPath = "C:\\WorkFolder\\Example.exe";

    WSH = new ActiveXObject("WScript.Shell");

    WSH.run(AppPath);



    Note that instead of the hard-coded path to the application, you can use user forms to specify it. To learn more about forms in script extensions, please see the Using Forms in Script Extensions Help topic.


    Thank you.

  • rsippel's avatar
    rsippel
    Occasional Contributor

    Hi,



    I'm confused again.


    In your example of the executable that loads the DLL into an assembly ("SampleAssembly") it looks like the EXE is a console program. Doesn't it need some MessageLoop to keep the EXE active and alert for requests? How are requests for the wrappers recognized/processed in the EXE (what's the code supposed to look like for this)? What should a wrapper function look like? How does the EXE call the DLL via the SampleAssembly? It would be very helpful to have a more complete example for the necessary EXE code.


    I assume that my script should be using the AppDomain approach to access this EXE, and then the EXE is to wrap calls to the DLL which has been loaded into the SampleAssembly object.


    I'm sorry for asking so many questions, but there is something that I am always missing. I keep getting more little pieces of the puzzle but it's like I have been building out from the corners. The chunks are getting bigger and growing towards each other but they are still disconnected. I need the pieces that will finish connecting the chunks and make it complete/workable. It seems like it should be simple. Maybe I'm mixing pieces from different puzzles/approaches.

  • Hello Rob,


    In your example of the executable that loads the DLL into an assembly ("SampleAssembly") it looks like the EXE is a console program. Doesn't it need some MessageLoop to keep the EXE active and alert for requests?


    The sample code in my previous message belongs to the window application. TestComplete cannot access the AppDomain object of console applications. Sorry for confusing you.


    As for a more detailed example of the necessary application code, you can try doing any of the following (in the examples below, I use the UserClassLib.dll library shipped along with TestComplete's Using .NET Classes example):


    1. You can add a reference to your DLL into a sample application project and create an object that returns a reference to an instance of a class declared in your DLL:




    using System;

    using System.Windows.Forms;

    using UserClassLib;



    namespace DLLSample

      {

      public partial class Form1 : Form

        {

        public Form1()

          {

          InitializeComponent();

          CSalary SalaryObj = new CSalary();

          }

     

        }

      }


    2. Or you can create an application containing an object that loads your DLL in the application domain:




    using System;

    using System.Windows.Forms;

    using System.Reflection;



    namespace DLLSample

      {

        public partial class Form1 : Form

        {

        public Form1()

          {

          InitializeComponent();

          Assembly SampleAssembly = Assembly.LoadFrom(AppDomain.CurrentDomain.BaseDirectory + "UserClassLib.dll");

          }

        }

      }


    While the sample application is running and the SalaryObj (or SampleAssembly) object exists in the application, you can access the namespaces defined in your DLL via the AppDomain(…).dotNet property from TestComplete tests (or TestComplete’s script extensions). For example:




    function Main()

    {     

      var p = Sys.Process("DLLSample");

      var dotNETObj = p.AppDomain("DLLSample.exe").dotNet;



      var Salary = dotNETObj.UserClassLib.CSalary.zctor();

      Salary.Initialize(6000, 0, 0, 0);

      ...



    }


    Please let us know whether any of these is helpful.

    Thank you.