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:

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(&gt);
		       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")


4/14/2011