Calling a DLL from a script tool
New in Python version 2.5 is the ctypes, a foreign function library. It provides C-compatible data types and allows calling functions in DLLs or shared libraries. Using the ctypes module in Python allows ArcObjects code written in C++ to be used in a geoprocessing script tool.
Using ctypes in Python allows you to easily change the parameters and types the script tool expects without having to recompile our C++ code. The ctypes module supports any C-callable function with basic data types, such as char, int, float, and double as well as structs and pointers. For more information on the ctypes module in Python 2.6.2, refer to 16.15 ctypes—A foreign function library for Python.
There are many benefits with calling a DLL from a Python script. You can leverage the finely grained ArcObjects classes in your geoprocessing tasks, your intellectual property is protected, and it is much easier to implement than having to use the IGPFunction2 and IGPFunctionFactory interfaces. You create your object in Python and call the execute method, passing in the required parameters from the script using the geoprocessing framework.
How it works
The steps are the following:
Create a C++ Win32 project in Visual Studio 2008 that exports a simple function with the prototype:
int GpExecutetool(char* parameter1, char* parameter2)
Be sure to include the ArcGIS\com\directory in the project and import the ArcObjects .olb files.
Create a script tool in a custom toolbox that validates the two parameters and passes them to the script.
Your Python script will do the following:
- Import arcpy and ctypes.
- Get the parameters from the script tool.
- Import the DLL into memory.
- Get a pointer to the function in the DLL.
- Specify the required argument types of functions exported from DLLs by setting the argtypes attribute as well as the return type.
- Pass the parameters to the C++ code in the DLL.
The details
The C++ project (named GPToolAsSimpleDLL for this example) is a simple Win32 project that adds an AREA field to the feature class and calculates the value.
The header file
#ifdef GPTOOLASSIMPLEDLL_EXPORTS #define GPTOOLASSIMPLEDLL_API extern "C"__declspec(dllexport) #else #define GPTOOLASSIMPLEDLL_API extern "C"__declspec(dllimport) #endif GPTOOLASSIMPLEDLL_API int GPexecutetool(char*, char*);
The GPToolAsSimpleDLL source file opens the specified polygon feature class and sets the specified field to the area of each polygon feature. Every geoprocessing function you write could be implemented with a simple C function entry point, all in the same DLL, along with script tool companions to expose each function to ArcToolbox.
GPTOOLASSIMPLEDLL_API int GPexecutetool(char* featureclassPath, char* fieldname) { // Convert char*s to bstring _bstr_t catalogPath; catalogPath = featureclasspath; _bstr_t newfieldname; newfieldname = fieldname; // Coinitialize GP utilities class IGPUtilitiesPtr ipUtil(CLSID_GPUtilities); // Feature class holder IFeatureClassPtr ipFeatureclass(0); HRESULT hr; // Try to fetch feature class from catalog path if (FAILED(hr = ipUtil->OpenFeatureClassFromString(catalogPath, &ipFeatureclass))) { return -1; } // Index position of the specified field long fieldIndex; ipFeatureclass->FindField(newfieldname, &fieldIndex); // Set up query filter and feature cursor IQueryFilterPtr ipFilter(CLSID_QueryFilter); IFeatureCursorPtr ipCursor; IFeaturePtr ipRow; IGeometryPtr ipShape; // Open search cursor on feature class ipFeatureclass->Search(ipFilter, VARIANT_FALSE, &ipCursor); // Iterate esriGeometryType gt; for (ipCursor->NextFeature(&ipRow); ipRow != NULL; ipCursor->NextFeature(&ipRow)) { // Get row's associated geometry ipRow->get_Shape(&ipShape); // Ensure we've got a polygon ipShape->get_GeometryType(>); if (gt != esriGeometryPolygon) return -2; // Get area IAreaPtr ipArea(ipShape); double area; ipArea->get_Area(&area); // Pop double into a variant VARIANT value; value.vt = VT_R8; value.dblVal = area; // Set double variant onto target field ipRow->put_Value(fieldIndex, value); // Save ipRow->Store(); } return S_OK; }
The Python script acts as a broker, accepting the two parameters from the script tool as text and sending them to the DLL function as char*, zero-terminated strings. The script also uses the AddField geoprocessing tool before the function in the DLL is called. This can make your workflow more robust by implementing your proprietary functionality in C++ and implementing the common tasks in Python.
import arcpy import ctypes # Get the parameters from the script tool dialog # shp = arcpy.GetParameterAsText(0) fieldName = arcpy.GetParameterAsText(1) # See if the field already exists in the feature class. # If not, add it. if len(arcpy.ListFields(shp, fieldName)) == 0: arcpy.AddField_management(shp, fieldName, "DOUBLE") # Import DLL into memory and get a pointer to the function # Note: be sure the DLL is in your Python search path # dll = ctypes.cdll.GPToolAsSimpleDLL perform_function = dll.GPExecutetool # Tell ctypes the function accepts two char* arguments # perform_function.argtypes = [ctypes.c_char_p, ctypes.c_char_p] # Tell ctypes the function return type # perform_function.restype = ctypes.c_int # Call the function in the DLL # retval = perform_function(shp, fieldName) # Check the return value. If a 0 returned, success! # if retval == 0: arcpy.AddMessage("Success") elif retval == 1: arcpy.AddError("Unable to open " + shp) elif retval == 2: arcpy.AddError("Not a polygon feature class")