Why Ada Ada is nice Pascal-like language with a strong emphasis in on writing high integrity, embedded code. Using Ada for 4NT / Take Command plug-ins brings you the following advantages: A free (beer and speech) compiler incl. debugger, IDE and all the trimmings is available. Full wide character support - in the Ada-Demo – unlike the Delphi demo – almost no 8<->16 bit character conventions takes place. Ada is designed to catch programming errors at compile time and is therefore well suited for development in an environment where debugging is painful. It you still have to hunt down bugs: there is a extensive trace framework available. Ada is a modern, fully object orientated language. Ada has build in support for multi tasking. Interfaces well with C. I myself have more then 10 years of experience in C and C++ programming and could write 4NT / Take Command plug-ins in C but I still prefer to use Ada. If you want to learn how to program in Ada have look at the award winning Wikibook "Ada Programming". Step by Step Guide The Demo Plug-in is distributed as source only - like the C++ and Delphi plug-in it's only reason of existence is teaching you to write your own plug-ins. Here a step by step guide of how to compile the plug-in: Download and Install GNAT (GNU Ada Translator) I suggest you use the GNAT GPL 2007-2 from Libre — or better. The Libre compiler comes with a nice installer which will supply you with the compiler, the IDE, debugger and binutils for win32. Apart from the compiler package you will need the win32ada library, dll2def.exe and gnumake-3.79.1-nt.exe. The later two are not installers - they are just copied into the bin directory of the compiler installation. Download and Unpack the Ada Demo Download the latest Ada Demo from it's project home page and unpack the archive at a place of your choice. Download the Plugin SDK Make sure you get the newest version. Unpack the content of the archive into the SDK directory of Ada-Demo. Create a GNU compiler collection compatible import library Normally this is a pretty simple task as GNAT comes with all the needed tools. However, while JPSoft uses WINAPI as calling convention but they strip the @nn postfix from the function names - which confuses the linker. The way it works is that you first create a def file using dll2def: Code: GNAT_HOME\bin\dll2def JPPATH\TakeCmd.dll >TakeCmd.def Then you edit the def file to add the missing @nn :-(. Last you create an GCC compatible import library using gnatdll. Code: GNAT_HOME\bin\gnatdll -k -e TakeCmd.def -d TakeCmd.dll Note that you would have the same trouble if you wrote a plug-in using the GCC C compiler. You might actually be worth off as usually comes without a copy of gnatdll. Also: Ada-Demo comes with a Def-file we prepared earlier :-). Compile and Link On the command line Inside the Ada-Demo directory you use the following command: gnat make -P Ada_Demo Using the IDE 1. Start Menu -> GNAT GPL -> 2007 -> GPS 2. Project -> Open -> Ada_Demo.gpr 3. Build -> Make -> All How does it work In order to create a Plugin you need to create a "Stand-alone-Library". While this is a long standing features of GNAT I suggest you use the GPL 2007-2 version. In this version the project manager will do all the dirty work so you won't need to do any of the tricky stuff like writing you own DLLMain or creating complex makefiles. Call back function Functions that are to be called by 4NT / TakeCommand need to be in the general format: Code: function My_Function_Name (Arguments: in Win32.PCWSTR) return Interfaces.C.int; pragma Export (Convention => Stdcall, Entity => My_Function_Name, External_Name => "MyFunctionName"); Arguments is a pointer to the first character of the command line. You should use the usual C to Ada string conventions. Functions and Variables, returning data should be declared: Code: function My_Function_Name (Arguments : access TakeCmd.Plugin.Buffer) return Interfaces.C.int; pragma Export (Convention => Stdcall, Entity => My_Function_Name, External_Name => "MyFunctionName"); TakeCmd.Plugin.Buffer is an array of 2048 byte where you can place the return value as C string. Intialize, Terminate Every plugin must implement and export the following three functions: Code: function InitializePlugin return Win32.BOOL; pragma Export (Convention => Stdcall, Entity => Initialize_Plugin, External_Name => "InitializePlugin"); function GetPluginInfo return TakeCmd.Plugin.LP_Plugin_Info; pragma Export (Convention => Stdcall, Entity => Get_Plugin_Info, External_Name => "GetPluginInfo"); function ShutdownPlugin(EndProcess: Win32.BOOL) return Win32.BOOL; pragma Export (Convention => Stdcall, Entity => Shutdown_Plugin, External_Name => "ShutdownPlugin"); For more detail on these three functions see the comments in the actual SDK. Plugin Infos Every plugin must define the PLUGININFO structure and return a pointer to it via the GetPluginInfo function. Amongst other things the PLUGININFO structure identifies to 4NT/TC the Internal Commands, Internal Variables and Variable Functions that the plugin implements. More comments on PLUGININFO can be found later in this file in the Type definitions. Internal Variable names in the "Implements" field of the PLUGININFO structure (and their corresponding function names) must begin with an underscore ('_'). Variable Function names in the "Implements" field of the PLUGININFO structure must begin with an @; the corresponding function must be prefixed by "f_". (This allows variable functions to have the same name as internal commands.) For example: Implements:= 'reverse,@reverse'; Entering the name "reverse" on the command line will invoke the command reverse() Entering the name "@reverse" on the command line will invoke the variable function f_reverse() Variable function names are limited to a maximum of 31 characters. Internal command names may be any combination of alphanumeric characters up to a maximum of 12 characters. The case of function names (for Internal Commands, Variable Functions and Internal Variables) in the "Implements" field of PLUGININFO and the corresponding function names must match exactly or 4NT/TC will not recognise them (e.g. if you name a function "Sample" but put "sample" in the Implements field of PLUGININFO it will not work). Function Calls 4NT and TC call Plugin functions as follows: Internal Commands are passed a pointer to a null terminated string containing the command line minus the name of the internal command. Variable Functions are passed a pointer to a null terminated string containing the argument(s) to the plugin function. Internal Variables are passed a pointer to an empty null terminated string which is used for output only. Returning from the Plugin: For Internal Commands, return the integer result (anything left in the arguments string will be ignored). For Variable Functions, copy the result string over the arguments string and return the integer result. The maximum length for the result string is 2K (2047 characters + null byte). The integer return can be: - 0 = success - < 0 = failure; error message already displayed by the Plugin function failure; error value should be interpreted as a system error and displayed by 4NT / TC - > 0 = failure; error value should be interpreted as a system error and displayed by 4NT / TC * For Internal Variables, copy the result string over the arguments string. There is no meaningful integer return value for Internal Variables. The maximum length for the result string is 2K (2047 characters + null byte). * There is a special return value ($FEDCBA98) that tells the parser to assume that the plugin decided not to handle the variable/function/ command. The parser then continues looking for a matching internal, then external. Note that you can use this return value to have your plugin modify the command line and then pass it on to an existing internal variable/function/command. An example of using this return value can be found in the "DIR" command later in this plugin. 4NT & Take Command will trap any exceptions occurring in the plugin, to prevent the plugin from crashing the command processor. An error message will be displayed and the plugin will return an exit code = 2. Note that all strings passed between 4NT/TC and the plugin are null terminated and are UNICODE. Version 8.0 of 4NT and Take Command introduced keystroke monitoring. A keystroke monitoring function will be called every time a key is pressed, and is passed a pointer to a structure containing information about the command line and the key that was pressed. Thus a plugin can watch for specific keystrokes being pressed. A keystroke monitoring function's name must be prefixed by "*" in the "Implements" field of the PLUGININFO record to identify it to 4NT and Take Command. This demonstration plugin includes a keystroke monitoring function. GNAT specific issues This demonstration plugin does not implement a DLLMain function as it is not necessary. When the plugin is loaded the code between the first "begin" and "end." is executed and any initialisation can be performed there. In most cases this is likely to be adequate and a DLLMain function is not required. If you write a plugin that needs to execute specific statements every time a process or thread attaches or detaches to or from the plugin it can be done by writing a DLLMain function and assigning its address to the DLLProc variable when the plugin is first loaded. See the GNAT documentation for further information on this technique. 4NT Plugins use sdcall - but without the added "@nn" postfix. GNAT however will add "@nn" postfixes to all stdcall functions and you will need to remove them at link time using the "-Wl,--kill-at" option. The Demo GNAT-Project-File already contains the needed flag. As mentioned all strings passed between 4NT/TC and the plugin are null terminated and are UNICODE. Since Both Ada 95 and Ada 2005 have support for Wide_Characters you might be the better option. The Plugin catches all exceptions before returning to 4NT / TakeCommand. Notes about this Demo This Demo writes some debugging output - which you should not do in a real plugin. Using TakeCmd.dll functions with Ada I have not yet translated the entire TakeCmd.h into Ada functions and procedures because it will take a long time. If you need to call a function(s) in TakeCmd.dll the simplest way is to add them yourself to takecmd.ads if they have not yet been added. This is an example of how to translate a TakeCmd.dll function to Ada. I'm using the HEAD command as an example. In TakeCmd.h the command is defined as: Code: int WINAPI HeadCmd( LPTSTR pszArguments ) which means that the function "HeadCmd" is called using the "WINAPI" calling convention (equivalent to "stdcall") and is passed a pointer to a null terminated string containing the command's arguments, and the command returns an integer value on completion. To use the above command from within your Ada plug-in you would make the following declaration: Code: function Head_Cmd (Arguments: in Win32.PCWSTR) return Interfaces.C.int; pragma Import (Convention => Stdcall, Entity => HeadCmd, External_Name => "HeadCmd"); If the declaration in TakeCmd.h begins with "void" it means there is no return value, which is equivalent to a "Procedure" declaration in Ada. For example, the TakeCmd.h declaration: Code: void WINAPI CrLf( void ); translates to the Ada declaration: Code: procedure CrLf; pragma Import (Convention => Stdcall, Entity => CrLf, External_Name => "CrLf"); This plugin contains an example of calling TakeCmd.dll functions in the command "Remark". Some functions in TakeCmd.dll manipulate the string that you pass to them within the buffer in which the string resides. Therefore, if the TakeCmd.dll function can return a longer string than is passed to it, you must make sure that you pass the string in a buffer that is large enough to provide working space and to hold the returned string if appropriate. An example of such a function is: Code: void WINAPI AddCommas( LPTSTR pszNumber ); which inserts the thousands separator into the supplied number. In Ada this would be declared as: Code: procedure AddCommas(Arguments: in Win32.PCWSTR); pragma Import (Convention => Stdcall, Entity => AddCommas, External_Name => "AddCommas"); When this procedure adds in the thousands separator the resulting string will be longer than the string that you started with, and so you must allow room for that expansion. The size of the buffer that you need cannot be precisely defined, and you may need to experiment. However, as a guideline I would suggest a buffer of at least three times the length of the string that you are passing to the function. A working example of calling this kind of function is included in the "usebuffer" function below. Note that if you use the following function from TakeCmd.dll: Code: int WINAPI Command( LPTSTR pszLine, int nReserved ); which calls the parser to expand and execute the supplied command then the buffer should be at least 2K and preferably 8K to 16K to allow for variable and alias expansion. TakeCmd packages TakeCmd Binding to the C function in TakeCmd.dll together with a few convience functions. TakeCmd.Plugin Type declarating used by the plugin binding. TakeCmd.Strings Some convience string operations for the use in Plugin. To_Ada Convert the string to Ada and if desired remove spaces from the begin and end. Also the string can be converted to upper case if casing is not important. Code: Arguments String to be converted To_Upper Convert to upper case Trim_Spaces remove unneded spaces. To_Win Convert the string to a Function / Variable return value. Or better paces it there since the new Ada 2005 "in-place-return" is used. Arguments String to be converted TakeCmd.Trace A full features trace module for use in both Ada and scripts. Multi-Tasking You can use both tasks and protected objects inside your plug-in. However 4NT / Take Command them self are not multi threading capable which leads to some restriction: Library level tasks won't work - use (access to) task types instead and create them inside the InitializePlugin and destroy them in the ShutdownPlugin function. Library level calls to library level protected objects won't work. Later library level protected objects will work ok. But the greatest restriction is that you can't call TakeCmd.dll functions inside a task. Calling them inside a protected object seem ok. Failing the above rule will either lead to a silent death or a complete hang of your 4NT / Take Command session. Standart_Output & Standart_Output Don't. Trying to output to Standart_Output or Standart_Output will result in a DEVICE_ERROR at best and a silent death of your 4NT / Take Command session at worse. Use QPuts instead.