IMAGE INTERFACE (png)

Introduction

Image processing means working with images, which means we need a way to read them into our programs and write out the results.  Before we can do any real work then, we must figure out how to interface with C libraries, namely libpng which can read and write in PNG format.  Sigh.  This isn't an exciting way to begin.

We'll begin by writing and testing the C code before duplicating the program in Chapel.  We'll follow this by tying the C data structure to Chapel to access the image directly, and we'll conclude this exercise by looking at how Chapel organizes code into modules and programs.

The directory for this section is png.  Programs can be built by doing make name, where the name is without the file suffix (.c or .chpl); we'll also give the full compiler command in the text.  Executables are put in the bin sub-directory; build holds auto-generated dependency lists and intermediate object files.  The sample image for this exercise is bear.png.

The files and image for this section are found here (as zip file).  A PDF copy of this text is here.

C Library (img_png_v1.c, test_png.c)

img_png is a C file that provides functions to read and write PNG images and a data structure that holds the image size and separate arrays for the R, G, and B planes.  This data structure, called rgbimage, is dynamically allocated and must be manually freed.  Functions take a pointer to an rgbimage pointer if they create or destroy an image, or just an rgimage pointer if they work on it.  Make sure the initial value for the pointer is NULL or else the allocation routine will assume it already holds an image and will try to free it, which will raise a segfault.

    rgbimage *img = NULL;

    read_PNG(filename, &img);

    free_rgbimage(&img);

img_png_v1 (the _v1 indicates we'll be modifying the code later, in this case to interface better with Chapel) contains three groups of functions.  The first provides PNG support: PNG_isa() tests if a file contains a PNG image; PNG_read() creates a new image with the picture; and PNG_write() writes a picture to a file.  The second group manages rgbimages: alloc_rgbimage() creates a black picture (ie. R, G, and B all zero) of a given size; and free_rgbimage() releases the memory.  Not calling free_rgbimage() is a memory leak.  The third group accesses pixels without knowing about the rgbimage data structure: read_rgb() gets the R, G, and B values at a point; and write_rgb() changes them.  This group is to get us started, until we figure out how to directly access C arrays from Chapel.

The code should be straightforward.  The PNG functions follow the guidelines in the library's documentation, including setjmp() and longjmp() for error handling.  The memory functions use calloc() and free(), and the access functions are array operations.  The color planes are stored row-by-row, packed into a 1D array, where a point (x,y) in an image with ncol columns is located at index xy

    xy = y * ncol + x;

test_png.c is a simple test program for these functions.  It reads an image, prints out the R, G, and B values at a pixel, then changes them to 1, 2, and 3 respectively before writing the result to a new file.  Compile it with

    gcc -c -o build/img_png_v1.o img_png_v1.c

    gcc -o bin/test_png test_png.c build/img_png_v1.o -lpng

or  make test_png

which compiles first img_png_v1.c, putting the object file in the build sub-directory and then test_png.c, putting the executable in bin.  Run it with

  bin/test_png bear.png tstimg.png 615 212

The point (615, 212) is the tip of the leaf below the bear's right ear.  It has R 83 G 156 B 25, which you can confirm in the program's output.  If you open tstimg.png you can verify that the pixel has changed to R 1 G 2 B 3.

 
 
 
 
 

C interface from Chapel (rw_png_v*.chpl)

The goal of this section is to duplicate the test_img.c program, but this time in Chapel.  That is, we'll show ho we can read an image, access and manipulate it, and save the result.  The program logic is trivial; the difficulty will be interfacing to the C world.  The concepts we'll need to understand are variable definitions and the Chapel basic types, procedure declarations to mirror the functions in the C library, and what Chapel requires to link to external code, both in the source file and when compiling.

Primitive Types

Chapel has a small list of primitive types: int and uint (unsigned); real, complex and imag(inary); bool(ean); string; and void.  Types can be given a size in bits.  Currently supported are

 

8

16

32

64

128

   bits

int

x

x

x

D

 

 

uint

x

x

x

D

 

 

real

 

 

?

?D

 

 

imag

 

 

?

?D

 

 

complex

 

 

 

?

?D

 

bool

x

x

x

x

 

   (default varies)

? means potentially supported depending on the hardware, D is the default bit depth, and x an otherwise allowed bit depth.  Sizes are written in parentheses after the type

    int(32)

Use 0x[0-9A-Fa-f]+ to represent a hexadecimal number, 0o[0-7] for an octal, and 0b[01] for a binary.  The x, o, and b can be capitalized, but anybody who uses 0O is evil and deserves the everlasting pain they will suffer.

An imaginary number is followed by i.  The format of complex numbers is 'a + bi' or (a, b), a tuple.  The real part of a complex variable c is gotten with c.re, the imaginary with c.im.  You can use these components on the left side of an assignment.

    c = -1.0 + 1.0i;

    writeln(“real part “ + c.re + “   img part “ + c.im);

    > real part -1.0   img part 1.0

    c.re = 2.0;

    writeln(“real part “ + c.re + “   img part “ + c.im);

    > real part 2.0   img part 1.0

The boolean values are true and false.  In conditional expressions for if, while, and do-while an integer is treated as false if 0 and true if non-zero, and a class as false if nil, true if not.

Strings only support ASCII.  Void is only used as an empty argument list or return type.

Variable Declarations

Chapel has three ways to declare a variable: either a type is provided and there is a default initialization value, or an initialization value is provided and the type is inferred from it, or both are given.  So

    var a : int(32);     /* default to 0 */

    var a = 1234;        /* int(64) */

The kind of the variable must precede the name.  There are three. var is a normal variable,  param a compile-time constant, and const a runtime constant.  A parameter can be assigned a primitive value, an enumerated type, or a simple expression using the normal math and comparison operators.  Several of the functions in the Math standard module are also available; they must be declared as a param output.  A constant can be of any type, but must be initialized and keeps that value for its lifetime.

Numbers default to 0, booleans to false, and strings to the empty string.  Classes default to nil.

A variable name is any alphanumeric character, the underscore, or dollar sign. It must begin with a letter or underscore.

Multiple variables can be combined in a comma-separated list with types and initializations shared.

    var a, b=-1, c, d=1, e: uint(8), f: string;

creates five 8-bit unsigned integers (a-e) and one string.  a and b are set to -1, c and d to 1, and e and f get the default.

There are two optional modifiers to the kind of variable.  One is extern, and we'll get to it soon.  The other is config.  This creates a configuration variable/parameter/constant that can be initialized from the command line. For example, from the Mandelbrot benchmark program (in CHPL_HOME/examples/benchmarks/shootout/mandelbrot.chapel)

    config const n = 200,           // the problem size

                 maxIter = 50,      // the maximum # of iterations

                 limit = 4.0,       // the limit before quitting

                 chunkSize = 1;     // the chunk size of the dynamic iterator

The Chapel compiler will automatically add command line arguments to change these values.  You can either pass --<cfgvar>=<val>, or -s<cfgvar>=<val> (without a space after the s).  If the value is a string with spaces, you must wrap it in quotes.

Try this.  The four lines of code are placed in a file and compiled, then the program is run.

    config param cfgparam : int = 1;

    config const cfgconst : int = 2;

    config const cfgvar : real = 2.178;

    writeln("cfgparam = ", cfgparam, "   cfgconst = ", cfgconst,

            "   cfgvar = ", cfgvar);

 

    chpl -o ex_config ex_config.chpl

    ./ex_config

    > cfgparam = 1   cfgconst = 2   cfgvar = 2.178

    ./ex_config --cfgvar=3

    > cfgparam = 1   cfgconst = 2   cfgvar = 3.0

    ./ex_config -scfgvar=3 --cfgconst=1

    > cfgparam = 1   cfgconst = 1   cfgvar = 3.0

The value on the command line must be valid for the type of the variable/constant.

    ./ex_config --cfgvar=3 --cfgconst=1.7

    > <command line setting of 'cfgconst'>: error: Unexpected character when

      converting from string to int(64): '.'

    ./ex_config --cfgvar=true

    > <command line setting of 'cfgvar'>: error: Unexpected character when

      converting from string to real(64): 't'

To set a parameter during compilation you must use the -s form.  Parameters cannot be changed from the command line during execution.

    ./ex_config --cfgvar=3 --cfgconst=1 --cfgparam=2

    > <command-line arg>:3: error: Unexpected flag: "--cfgparam=2"

    chpl --cfgparam=9 -o ex_config2 ex_config.chpl

    > Unrecognized flag: '--cfgparam=9' (use '-h' for help)

    chpl -scfgparam=9 -o ex_config2 ex_config.chpl

    ./ex_config2

    > cfgparam = 9   cfgconst = 2   cfgvar = 2.178

-h or --help print a table listing all the options that are available.

    ./ex_config2 -h

> FLAGS:

> ======

>   -h, --help            : print this message

>   -a, --about           : print compilation information

>   -nl <n>               : run program using n locales

>                           (equivalent to setting the numLocales config const)

>   -q, --quiet           : run program in quiet mode

>   -v, --verbose         : run program in verbose mode

>   -b, --blockreport     : report location of blocked threads on SIGINT

>   -t, --taskreport      : report list of pending and executing tasks on SIGINT

>   --gdb                 : run program in gdb

>   -E<envVar>=<val>      : set the value of an environment variable

>

> CONFIG VAR FLAGS:

> =================

>   -s, --<cfgVar>=<val>  : set the value of a config var

>   -f<filename>          : read in a file of config var assignments

>

> CONFIG VARS:

> ============

> Built-in config vars:

>        printModuleInitOrder: bool

>       dataParTasksPerLocale: int(64)

>   dataParIgnoreRunningTasks: bool

>       dataParMinGranularity: int(64)

>                    memTrack: bool

>                    memStats: bool

>                    memLeaks: bool

>               memLeaksTable: bool

>                      memMax: uint(64)

>                memThreshold: uint(64)

>                      memLog: c_string

>                 memLeaksLog: c_string

>                  numLocales: int(64)

>

> config config vars:

>                    cfgconst: int(64)

>                      cfgvar: real(64)

This example is in ex_config.chpl.  Compile and run it with

    chpl -o bin/ex_config ex_config.chpl

or  make ex_config

    bin/ex_config

There is one quirk of the initialization of config variables if we put more than one on the same line.  Initialization proceeds from left to right along the list, with the variable getting the value of the previous.  If we provide a value for only one, then all others to its right will also get that value.  In this example setting x also changes y, but y does not change x.  z, listed on a separate line in the source, only changes when listed on the command line.

    config const x, y = -1;

    config const z = -1;

    writeln("x = ", x, "   y = ", y, "   z = ", z);

 

    ./ex_init

    > x = -1   y = -1   z = -1

    ./ex_init --x=3

    > x = 3   y = 3   z = -1

    ./ex_init --y=3

    > x = -1   y = 3   z = -1

    ./ex_init --z=3

    > x = -1   y = -1   z = 3

In short, config variables with initialization conditions should be put on separate lines.  You'll find the example in ex_init.chpl.  Compile and run it with

    chpl -o bin/ex_init ex_init.chpl

or  make ex_init

    bin/ex_init

Function Prototypes

One more piece we'll need for this exercise is how to declare the C-side functions for Chapel.  They look like:

   proc fnname(arg1 : type1, arg2 : type2) : returntype

but have a number of options.

1. You can call procedures with the arguments named:

    proc tstfn1(arg1 : int, arg2 : real) {

      writeln("tstfn1:  arg1 = ", x, "   arg2 = ", y);

    }

    tstfn1(arg2=3, arg1=1);

    > tstfn1:  arg1 = 1   arg2 = 3

2. You can provide default values for the arguments:

    proc tstfn2(arg1 : real = 2.718, arg2 : string = "dummy string") {

      writeln("tstfn2:  arg1 = ", x, "   arg2 = ", y);

    }

    tstfn2();

    > tstfn2:  arg1 = 2.718   arg2 = "dummy string"

    tstfn2(3.14);

    > tstfn2:  arg1 = 3.14   arg2 = "dummy string"

    tstfn2(arg2="a new string")

    > tstfn2:  arg1 = 2.718   arg2 = "a new string"

    tstfn2(arg1=1.41, "another string");

    > tstfn2:  arg1 = 1.41   arg2 = "another string"

    tstfn2("another string", arg1=1.41);

    > tstfn2:  arg1 = 1.41   arg2 = "another string"

In the first output line we see the default values for both arguments.  In the second we call by position, which means supplying a value for arg1 and using the default for arg2.  To use the default for arg1 we need to name arg2 in the third call.  The last two uses show that it's possible to swap the order of a named and positional argument: Chapel will evaluate the named argument as if it were passed at its position in the argument list.  arg1 pushes the unnamed argument to the second position, which aligns with arg2.

3. You can omit the parentheses if there are no arguments to the procedure, or just put the parentheses with nothing between them.  You must call the procedure with or without parentheses as it was defined.

    proc tstfn3a { writeln("tstfn3a:  no args"); }

    proc tstfn3b() { writeln("tstfn3b:  no args"); }

    tstfn3a;        /* tstfn3a() is illegal */

    > tstfn3a:  no args

    tstfn3b();      /* tstfn3b is illegal */

    > tstfn3b:  no args

4. The arguments can be qualified by an "intent" which govern how the argument is passed to/from the procedure.

5. To return a value from the procedure, use a return with an expression.  If you specify a return type for the procedure (as in our introductory example), all return statements must produce either a value of that type or one that can be implicitly cast to it.  If you do not specify a type, then all return statements are examined.  There must be an implicit cast of all of them to one type, which becomes the return type of the procedure.  If there is no return, the type is void.  (So the procedures in the examples above all return void.)

    proc tstfn5(arg1 : int) {

      if (arg1 < 5) {

        return 5;            /* an int */

      } else if (arg1 < 10) {

        return 6.0;          /* a real */

      } else {

        return 1.0 + 1.0i;   /* a complex, the return type */

      }

    }

    var ret5 = tstfn5(3);

    writeln("tstfn5:  real part ", ret5.re, "   complex ", ret5.im);

    > tstfn5:  real part 5.0   complex part 0.0

    ret5 = tstfn5(7);

    writeln("tstfn5:  real part ", ret5.re, "   complex ", ret5.im);

    > tstfn5:  real part 6.0   complex part 0.0

    ret5 = tstfn5(11);

    writeln("tstfn5:  real part ", ret5.re, "   complex ", ret5.im);

    > tstfn5:  real part 1.0   complex part 1.0

Return types may also get intents.  There are three.  The ref intent means the procedure returns a reference to a variable.  When such a procedure call is placed on the left side of an equals sign, or used as an (in)out or ref argument in another call, then the referred variable's value changes.  When it is used on the right side, the procedure returns the existing value of the variable.  The param intent evaluates at compile time and must create one of the simple expressions acceptable for a parameter.  The type intent means the procedure will return a type such as uint(8).  This can be used for generic programming.  The procedure will only be evaluated during compilation.

6. Procedures may be nested inside others, with the inner only visible within the outer's scope.  Nested functions may refer to variables in the outer procedure.  Names in the inner procedure will shadow the outer.

    proc tstfn6(arg1 : int) {

      const val1 = 3.14;

      proc tstfn6b(arg1 : real) {

        writeln("tstfn6:  arg1 = ", arg1, "   val1 = ", val1);

      }

      tstfn6b(arg1 + val1);

    }

    tstfn6(3);

    > tstfn6:  arg1 = 6.14   val1 = 3.14

7. You can precede the proc with the keyword inline.  The procedure will be inlined at every call site.

8. Procedures, including many operators, may be overloaded; the best match to the arguments in the caller will be used.  Operators keep their precedence and arity, the number of arguments they take.  The language specification contains a table with the list of operators that may be overloaded.

9. You can have a variable number of arguments, indicated with three dots.  This, combined with generic procedures and type arguments, allows a level of meta-programming which we'll look at shortly

You cannot combine variables with the same type in the declaration.

    proc tstfn(arg1, arg2 : int) { }

is not allowed, you have to put the type after each argument.

The code samples for this section are in ex_fn.chpl.  Compile and run them with

    chpl -o bin/ex_fn.chpl

or  make ex_fn

    bin/ex_fn

External Linkage (rw_png_v1.chpl)

Now the pieces are in place.  The general outline of this first program will be:

To access things C-side, we need to declare them in the Chapel program and provide the C header and object files when we compile.  The declarations follow the variable and function formats we've already described.  We only need to put the extern keyword before them.

The five C prototypes we need are:

    int PNG_read(char *inname, rgbimage **img);

    int PNG_write(char *outname, rgbimage *img);

    int read_rgb(rgbimage *img, int x, int y, uchar *r, uchar *g, uchar *b);

    int write_rgb(rgbimage *img, int x, int y, uchar r, uchar g, uchar b);

    void free_rgbimage(rgbimage **);

(where uchar is a typedef for unsigned char).  Let's start with write_rgb() as it's the easiest.

If the C program uses the standard types that encode the bit size in their name (int32_t) then it's safe to use the corresponding Chapel type int(32) directly.  Otherwise the recommendation is to use the types c_<typename> (c_int, c_uint, c_char, c_uchar) because there might be a mismatch.

So, ignoring the pointer for a moment, the write_png() prototype becomes

    extern proc write_rgb( ???, x : c_int, y : c_int,

                          r : c_uchar, g : c_uchar, b : c_uchar) : c_int;

How about the image pointer?  First we need to handle the type.  If all we're doing is passing around a pointer between C functions, without needing to access the data from Chapel, then we can define an empty type alias that tells Chapel to only handle the pointer.  The data structure behind the pointer is "opaque", and can only be assigned to other opaque variables of the same type or used as an argument to a C function.  That's all we need for now, so the command for the type alias is

    extern type rgbimage;

For the pointer, there are a couple ways to approach it.  There is a type c_ptr(<type>), which can also be constructed from a Chapel variable by calling c_ptrTo(<variable>), where it gets the type from the variable.  We can also use a ref intent on the argument, which effectively means a pointer will be used.  Since we don't have a variable with the rgbimage, only a pointer to a blob of that type, we use the c_ptr option. The prototype becomes

    extern proc write_rgb(img : c_ptr(rgbimage), x : c_int, y : c_int,

                          r : c_uchar, g : c_uchar, b : c_uchar) : c_int;

The PNG_write() function is now clear.  Chapel defines the c_string type, which we'll use for the first argument, and the same type of pointer as we just talked about for the second.  The prototype is

    extern proc PNG_write(fname : c_string, img : c_ptr(rgbimage)) : c_int;

For read_rgb(), we know how to represent the first three arguments.  What about the pointers to uchar for the RGB triple, where we are returning the values to the variable?  This does sound like we want the ref intent for the pointer.  First we'll need three variables to hold the values, and then declare the corresponding arguments as refs.

    var rpix, gpix, bpix : c_uchar;

    extern proc read_rgb(img : c_ptr(rgbimage), x : c_int, y : c_int,

                         ref r : c_uchar, ref g : c_uchar, ref b : c_uchar);

The final two prototypes require a double pointer to receive the pointer to the memory with the allocated image.  This must be set to NULL initially or the PNG functions will assume there's an image already there and will try to free it.  The equivalent to this in Chapel is to declare a pointer to the type as a variable and to pass that variable as a ref argument, ie. a pointer to the variable which is a pointer.  When we declare the variable we want to initialize it to nil.  Only classes can be assigned nil, which is also their default value; this is a placeholder until the class is actually instantiated.  In this next initialization, then, we're foreshadowing that c_ptr is implemented as a class, as we'll soon see.

    var rgb : c_ptr(rgbimage) = nil;

    extern proc PNG_read(fname:c_string, ref img:c_ptr(rgbimage)) : c_int;

    extern proc free_rgbimage(ref img:c_ptr(rgbimage)) : void;

This defines the five prototypes for the C interface, and four local variables that we need for arguments modified by the functions.

Following the command line for test_png, we will want four options:

   config const inname : c_string;

   config const outname : c_string;

   config const x, y : c_int;

config means we can supply the value on the command line (and must, because this version of the program will crash if we don't provide them). We don't change the value so const is appropriate.  We need to use the C types here or the compiler won't be able to match argument types when calling the functions.  Chapel will cast the command line arguments correctly.

The last thing that's left is the logic: calling the five functions and printing the RGB values we've retrieved.

    PNG_read(inname, rgb);

    read_rgb(rgb, x,y, rpix, gpix, bpix);

    writef("\nAt %4i,%4i   R %3u  G %3u  B %3u\n", x,y, rpix,gpix,bpix);

    write_rgb(rgb, x,y, 1, 2, 3);

    PNG_write(outname, rgb);

    free_rgbimage(rgb);

The writef() statement is the equivalent of C's printf() where i is for an int and u a uint.  Note that we're ignoring the return value of the functions for now.  If something goes wrong, it will go horribly wrong.  Try running the program without any command line options and you'll get a segmentation fault because we ignore the error code that PNG_read() will return (inname is an empty string, so it can't read that file) and when read_rgb() runs it crashes because the rgb variable is nil.

To compile the program we need in addition to the program (rw_png_v1.chpl) the header file for the C code (img_png.h), the object file (build/img_png.o), and a link to the PNG library.  To compile and run the program use

    chpl -o bin/rw_png_v1 rw_png_v1.chpl img_png_v1.h build/img_png_v1.o -lpng

or  make rw_png_v1

    bin/rw_png_v1 --inname=bear.png --outname=tstimg.png --x=615 --y=212

    > At  615, 212   R  83  G 156  B  25

You can inspect tstimg.png, or compare it to the test_png output to see that it's the same.

Aside: Embedding C Requirements (rw_png_v1b.chpl)

In the October 2015 release Chapel added support for embedding C file requirements inside the Chapel source file instead of on the compiler command line.  The declaration in the source file is a comma-separated list of strings following the require keyword that specify header, C source, and object files or libraries.  Header files are included, source files are compiled as Chapel normally would, and object files and libraries are linked.  For this program we can write

    require "img_png_v1.h", "build/img_png_v1.o", "-lpng";

which you'll find on the compiler command above.  That then becomes

    chpl -o bin/rw_png_v1b rw_png_v1b.chpl

or  make rw_png_v1b

Whether you find it better to track dependencies in the source or make file is a question of style.  We'll keep listing them on the compilation command, because we need to list the dependencies for the make target anyway.

Structural Types (rw_png_v2.chpl)

The rgbimage type was opaque: Chapel has no idea what's in it, and only passes a pointer around.  Can we gain access to the fields of the structure? To do that we'll need a structural type.

Chapel has three structural types: class, record, union.  Classes and records are very similar, except classes are handled by reference and records by value.  We'll see the difference clearly when we modify img_png_v1.c and rw_png_v1.chpl so we can directly access the members of the rgbimage struct. All three allow variables, types, and procedures to be declared inside.  Variables within these are called 'members' or 'fields' and procedures 'methods'.  Using the terminology from object-oriented programming is deliberate, as classes and records also support inheritance.  These fields are accessed by dot notation, or instance.method().  (The language spec describes a fourth type, tuples, as structural, but they have a different, simplified, layout, without methods or named members.  We'll use tuples in the next exercise.)

The examples for this section are in ex_struct.chpl.  Compile and run  it with

    chpl -o bin/ex_struct ex_struct.chpl

or  make ex_struct.chpl

    bin/ex_struct

Unions are the simplest of the three.  They are declared with the union keyword, possibly preceded by extern, followed by a name and then any declarations within braces.

   union numbers {

     var svar : int(16);

     var ivar : int(32);

     var lvar : int(64);

     var rvar : real;

     var cvar : complex;

  }

Unions can only contain a value in one field at a time.  Writing to another field will unset the first.  As an example, let's assume we have two variables of this union type:

  var exunion, cpunion : numbers;

  exunion.ivar = 1234;

  writef("assigned union int %i\n", exunion.ivar);

  > assigned union int 1234

If we assign a union variable to another, then the active field's value is copied and that field is set active.

  cpunion = exunion;

  writef("copy has int field %i\n", cpunion.ivar);

  > copy has int field 1234

This copy has been made by value; there is no link between cpunion and exunion at this point.  Changing the active field in exunion will not affect cpunion.

  exunion.cvar = 1.0 + 2.0i;

  writef("changed union to complex %z\n", exunion.cvar);

  writef("copied union still int   %i\n", cpunion.ivar);

  > changed union to complex 1 + 2i

  > copied union still int   1234

It is a runtime error to access a field that has not been set.

  writef("union now has no int     %i\n", exunion.ivar);

  > ex_struct.chpl:36: error: halt reached - illegal union access

When a union is initialized no field is set.  The variable declarations inside should not be given initialization conditions.  Although the compiler will not complain, you will not be able to access an initialized field (the runtime will give an illegal union access error), as if the initialization does not set it active.  You will still need to assign something to one field before accessing it.

Records and classes are declared the same way, except for the keyword and the possibility of specifying a superclass for a class.  Records and classes differ in a few ways, the main ones being how they are initialized and assigned.  If we think of a class as a pointer to a structure, and a record as a structure, then we understand many of the differences.  A pointer can take the NULL value – variables of a class type can take nil, records cannot.  Storage for records is known when the variable is declared and is reclaimed when the variable leaves scope, while a class must be manually constructed and destroyed.  Class variables are assigned by copying a pointer and so the left hand side refers to the right after the assignment.  Records are copied by value and are independent afterward.

Let's look at some examples to see these points.

A record implementation of the rgbimage struct in img_png.h might look like

    record rgbimage_rcd {

      var ncol : c_int;

      var nrow : c_int;

      var npix : c_int;

      var r : c_ptr(c_uchar) = nil;

      var g : c_ptr(c_uchar) = nil;

      var b : c_ptr(c_uchar) = nil;

    }

We're using the C types (see the previous section) to lay the groundwork for the new version of the rw_png program we'll be making.  For each element of the structure, Chapel automatically makes a single method with the name of the member.  It acts as both a  getter and setter.  Do you remember from the procedure discussion that you can omit the parentheses after a procedure if it takes no argument, and that any call must also omit them?  That's essentially what's happening here. The method has a ref return intent so that if it's placed on the left side of an assignment statement the member is modified, and on the right it returns the value.  With one procedure we have both the getter and the setter.

When we declare a variable as an instance of this record, it is initialized member by member following the normal rules, ie. if an initialization expression has not been provided, then the type's default value is used. We can also assign a record to a variable or use it in an expression by using the keyword new before the record name, which allows us to access its default constructor.  The compiler automatically generates the constructor, where the arguments are provided in the same order as the fields and given their names.

    var exrcd = new rgbimage_rcd(ncol=300, nrow=400);        /* default npix=0 */

    var cprcd : rgbimage_rcd;                /* default ncol=0, nrow=0, npix=0 */

To set a field, simply assign it a value.

    cprcd.npix = 100;

    writef("exrcd: %4i x %4i (= %6i npix)   nil r? %s\n",

           exrcd.ncol,exrcd.nrow, exrcd.npix, (nil == exrcd.r));

    writef("cprcd: %4i x %4i (= %6i npix)   nil r? %s\n",

           cprcd.ncol,cprcd.nrow, cprcd.npix, (nil == cprcd.r));

    > exrcd:  300 x  400 (=      0 pix)   nil r? true

    > cprcd:    0 x    0 (=    100 pix)   nil r? true

The compiler will generate default =, ==, and != methods.  Each is applied member by member with the usual behavior.  Because records copy by value, the assignee is kept separate from the assigner.

    writef("ex == cp? %s\n", (exrcd == cprcd));

    > ex == cp? false

 

    cprcd = exrcd;

    writeln("copy exrcd to cprcd");

    writef("exrcd: %4i x %4i (= %6i npix)   nil r? %s\n",

           exrcd.ncol,exrcd.nrow, exrcd.npix, (nil == exrcd.r));

    writef("cprcd: %4i x %4i (= %6i npix)   nil r? %s   == exrcd? %s\n",

           cprcd.ncol,cprcd.nrow, cprcd.npix, (nil == cprcd.r),

           (exrcd == cprcd));

    > copy exrcd to cprcd

    > exrcd:  300 x  400 (=      0 pix)   nil r? true

    > cprcd:  300 x  400 (=      0 pix)   nil r? true   == exrcd? true

 

    exrcd.ncol = 600;

    exrcd.nrow = 800;

    writeln("set exrcd ncol, nrow");

    writef("exrcd: %4i x %4i (= %6i npix)   nil r? %s\n",

           exrcd.ncol,exrcd.nrow, exrcd.npix, (nil == exrcd.r));

    writef("cprcd: %4i x %4i (= %6i npix)   nil r? %s   == exrcd? %s\n",

           cprcd.ncol,cprcd.nrow, cprcd.npix, (nil == cprcd.r),

           (exrcd == cprcd));

    > set exrcd ncol, nrow

    > exrcd:  600 x  800 (=      0 pix)   nil r? true

    > cprcd:  300 x  400 (=      0 pix)   nil r? true   == exrcd? false

The npix value for cprcd is overwritten during the assignment, but the fields are then independent of changes to exrcd.

Fields are only accessible through an instance of a record or class.  Identifiers declared param will not be visible.

Just as procedure arguments and return values have intents, methods have an optional qualifier.  A type intent makes a static method.

    class num {

      var v : int;

 

      proc type dump(msg : string) {

        writeln(msg);

      }

    }

 

    num.dump("calling without an instance")

    > calling without an instance

Such a method cannot be called on an instance of the class.

    var n = new num(3);

    n.dump("or via an instance");

    > ex_method.chpl:18 error: unresolved call 'num.dump("or via an

      instance")'

    > ex_method.chpl: 5: note: candidates are: num.type dump(msg:string)

A ref intent means the argument, an implicit this or the instance itself, is passed-by-reference and can be changed.  In this example we replace the existing instance with a completely new one.  The %t format specifier prints an object-dependent description.  Note that you can declare a method outside its class by fully qualifying its name.

    proc ref num.changeInst() {

      this = new num(5);

    }

 

    var oldn = n;

    writef("original instance %t (same as old? %s)\n", n, (n == oldn));

    > original instance {v = 3} (same as old? true)

    n.changeInst();

    writef("new instance %t (same as old? %s)\n", n, (n == oldn));

    > new instance {v = 5} (same as old? false)

There is a third method intent, param, that means it can only be applied to a compile-time parameter.  You can define such methods on custom classes as we have done here, or the built-in types.

Compile and run the examples for method intents with

    chpl -o bin/ex_method ex_method.chpl

or  make ex_method

    bin/ex_method

Our class example will run the same with two differences.  First, we can't simply declare a variable as a class and have it appear; we must make an instance with the new keyword.  The comments about constructors generated by the compiler or the program still apply.

    class rgbimage_cls {

      var ncol : c_int;

      var nrow : c_int;

      var npix : c_int;

      var r : c_ptr(c_uchar) = nil;

      var g : c_ptr(c_uchar) = nil;

      var b : c_ptr(c_uchar) = nil;

    }

   

    var excls : rgbimg_cls;

    writef("excls nil post-declaration? %s\n", (nil == excls));

    > excls nil post-declaration? true

 

    excls = new rgbimg_cls(200, npix=1000, ncol=5);

    writef("new excls:     %4i x %4i (= %6i npix)   nil r? %s\n",

           excls.ncol,excls.nrow, excls.npix, (nil == excls.r));

    > new excls:        5 x  200 (=   1000 pix)   nil r? true

 

    excls.ncol = 10;

    excls.npix = 2000;

    writef("excls changed: %4i x %4i (= %6i npix)   nil r? %s\n",

           excls.ncol,excls.nrow, excls.npix, (nil == excls.r));

    > excls changed:   10 x  200 (=   2000 pix)   nil r? true

The example variable defaults to nil until we create a new instance of the class.  Accessing and changing member values happens the same way. We can of course instantiate the class when we declare the variable, but watch what happens after we create the copy.

    var cpcls = excls;

    writef("make cpcls:    %4i x %4i (= %6i npix)   nil r? %s   == excls? %s\n",

           cpcls.ncol,cpcls.nrow, cpcls.npix, (nil == cpcls.r),

           (cpcls == excls));

    > make cpcls:      10 x  200 (=   2000 pix)   nil r? true   == excls? true

 

    excls.ncol = 20;

    excls.npix = 4000;

    writef("excls changed: %4i x %4i (= %6i npix)   nil r? %s\n",

           excls.ncol,excls.nrow, excls.npix, (nil == excls.r));

    writef("cpcls now:     %4i x %4i (= %6i npix)   nil r? %s   == excls? %s\n",

           cpcls.ncol,cpcls.nrow, cpcls.npix, (nil == cpcls.r),

           (cpcls == excls));

    > excls changed:   10 x  400 (=   4000 pix)   nil r? true

    > cpcls now:       10 x  400 (=   4000 pix)   nil r? true   == excls? true

The change to excls' ncol and npix appears in cpcls because both variables point to the same underlying memory.

(By the way, the reason for always testing if the r field is nil is because the Chapel write routines don't understand how to print a c_ptr.  The compilation will fail.)

You can define constructors and destructors for both records and classes.  Constructors have the name of the structure and take any arguments that you want, which are supplied when instantiating with new.  Destructors have the name of the structure preceded by a tilde and take no arguments.

    class image_cls {

      var ncol, nrow, npix : int;

 

      proc image_cls(numcol : int, numrow : int) {

        ncol = numcol;

        nrow = numrow;

        npix = numcol * numrow;

      }

 

      proc ~image_cls() {

        writeln("freeing allocated image");

      }

    }

 

    var eximg = new image_cls(10, 20);

    writef("image size %4i x %4i (= %6i npix)\n",

           eximg.ncol,eximg.nrow, eximg.npix);

    > image size   10 x   10 (=    200 npix)

Be aware that there is one critical difference between records and classes.  Records are automatically created and destroyed, classes are handled manually.  You can use new with a record to access a constructor; if you provide a custom constructor the default version will not be available.  Without using a new a record's fields are initialized to their default per their type.  When a record variable leaves scope its destructor, default or custom, executes – you cannot do this yourself.  On the other hand, a class variable is nil when declared and you must use new to create an instance.  You must use delete to call the destructor, and if you do not you will leak memory.

    class test_cls {

      var tmp : int;

 

      proc ~test_cls() {

        writeln("  called class destructor, tmp was ", tmp);

      }

    }

    record test_rcd {

      var tmp : int;

 

      proc ~test_rcd() {

        writeln("  called record destructor, tmp was ", tmp);

      }

    }

 

    proc test_destruct1() {

      var c = new test_cls(3);

      var r = new test_rcd(2);

 

      writeln("in test_destruct1:");

      /* This leaks c. */

    }

         

    proc test_destruct2() {

      var c = new test_cls(2);

      var r = new test_rcd(3);

 

      writeln("in test_destruct2:");

      delete c;

    }

    test_destruct1();

    > in test_destruct1:

    >   called record destructor, tmp was 2

    test_destruct2();

    > in test_destruct2:

    >   called class destructor, tmp was 2

    >   called record destructor, tmp was 3

So how do we apply this to our example?  We have a problem that rgbimage in img_png.h was essentially defined as a record, but we always use a pointer to it.  In other words, instead of a record we really want to work with a class.  This requires changing the C-side structure to

    typedef struct __rgbimage {

      int ncol, nrow;

      int npix;

      uchar *r, *b, *g;

    } _rgbimage, *rgbimage;

This pattern:

    typedef struct __D {

    } _D *D;

is required by the Chapel compiler.  In other words, the structure name (D) must be a pointer to a structure whose type alias is the same except for a leading underscore (_D) and whose name has two leading underscores (__D). The edit can be found in img_png_v2.h.

Only the class members that match the C structure elements are linked.  The Chapel class can leave some out, in which case they aren't accessible.  The class can also add members.  This also is true for methods.

To access the members in Chapel we delete the empty type definition for rgbimage ('extern type rgbimage;') and declare an extern class:

     extern class rgbimage {

       var ncol : c_int;

       var nrow : c_int;

       var npix : c_int;

       var r : c_ptr(c_uchar);

       var g : c_ptr(c_uchar);

       var b : c_ptr(c_uchar);

 

       proc ref ~rgbimge() {

         free_rgbimage(this);

       }

     }

We'll use a variable to this class, but not instantiate it because the memory allocation is done C-side in PNG_read().  We've added a destructor that wraps free_rgbimage() to release the memory when we're done with the class (by calling delete on it); this way we do not call free_rgbimage() directly.  The ref between the proc and destructor name is called a “this” intent and applies to the this keyword.  Without it you can only access the class instance, with it you can change the instance, which is what we need to do.  Remember we will have to call delete when done.  This will replace the free_rgbimage() call at the end of rw_png_v1.

Now, how to access the image arrays without going through read_rgb() or write_rgb()?  If you look in CHPL_HOME/modules/standard/SysBasic.chpl, you'll find the definitions for the C equivalent types, including c_ptr.  It turns out this is a class with two member functions:

    class c_ptr {

      /* The type that this pointer points to */

      type eltTye;

      /* Retrieve the i'th element (zero based) from a pointer to an array.

         Does the equivalent of ptr[i] in C.

      */

      inline proc this(i: integral) ref {

        return __primitive("array_get", this, i);

      }

      /* Get element pointed to directly by this pointer.  If the pointer

         refers to an array, this will return ptr[0].

      */

      inline proc deref() ref {

        return __primitive("array_get", this, 0);

      }

    }

Classes support a method this in addition to the member this, and the method allows us to treat the instance as a procedure.  In other words, we can write the instance variable followed by arguments inside parentheses, just like a procedure call.  Here the method has a ref return intent, so it acts as both a getter and a setter for the an array element using the internal Chapel function array_get.  If rgb is an instance of our rgbimage class, then

    rgb.r(xy)

will return the pixel value at index xy and

    rgb.r(xy) = newval;

will change the value.  

We can now replace our call to read_rgb()

    read_rgb(rgb, x, y, rpix, gpix, upix);

with

    const xy = (y * rgb.ncol) + x;

    rpix = rgb.r(xy);

    gpix = rgb.g(xy);

    bpix = rgb.b(xy);

xy here is just the conversion we've seen from a 2D pont to a 1D array index.

Similarly, the call to write_rgb()

    write_rgb(rgb, x, y, 1, 2, 3);

becomes

    const xy = (y * rgb.ncol) + x;

    rgb.r(xy) = 1;

    rgb.g(xy) = 2;

    rgb.b(xy) = 3;

The new version of the program can be found in rw_png_v2.chpl.  Compile and run it with

    chpl -o bin/rw_png_v2 rw_png_v2.chpl img_png_v2.h build/img_png_v2.o -lpng

or  make rw_png_v2

    bin/rw_png_v2 --inname=bear.png --outname=tstimg.png --x=615 --y=212

The output will be the same as img_png_v1 and test_png except you will also see the size of the image, showing that we can directly access the image data.

Modules (rw_png_v3.chpl)

If we forget to pass arguments on the command line, or cause an error by giving the wrong input file name for example, the program will crash.  We should at least fail gracefully and clean up after ourselves.  (Our policy here is to free any memory we've allocated, even after an error, which helps with memory leak checks).  Improving this will introduce us to Chapel's module system.

Modules are a simple namespace.  They can declared implicitly or explicitly. An explicit declaration contains the keyword module followed by a name and a set of statements wrapped in braces:

    module example {

    }

Any symbols that are defined within the braces are scoped to the module. An implicit declaration uses the name of the file, less the '.chpl' extension, as the module name.  So the two programs we've looked at so far have actually defined two modules, rw_png_v1 and rw_png_v2, with all variables and procedures local to them.  Note that if the file name does not form a legal identifer (alphanumeric plus underscore and dollar sign) then the module cannot be referenced.

There may be multiple modules in a file, and they may nest.  In this case the inner modules see all symbols in the outer, but the inner symbols must be qualified with the module name when used in the outer scope.

    module outerexample {

      var outervar : int(64);

      /* Must access innvervar with innerexample.innervar. */

      module innerexample {

        var innervar : int(64);

        /* Can access outervar without qualifying the name. */

      }

    }

If there are symbols at the top file level outside a module, and a module is also defined, then that module is nested inside the intrinsic module.

To use a module, give the keyword use and the name.

    use example;

If only an inner module is needed, provide its fully qualified name.

    use innerexample;    /* outerexample is not accessible. */

You do not need to qualify a symbol in that module, ex.

    innervar = 10;

But you can always provide a fully qualified name to access anything.

    outerexample.outervar = 11;

Symbols within modules, either variables or procedures, may be hidden from access outside the module by prefacing them with private.  If public or no keyword is used, then they will be visible.  If modules are nested then the inner can see the private symbols in the outer, but not the other way around.  Note that you cannot use a fully qualified name to gain access.  Modules themselves may also be declared public or private by putting the qualifier before the module keyword.

You'll see the private keyword in use later when we pull common code into library files, which will start with ip_.  An example for this section is ip_color_v1.chpl.

You do not have to list files with modules in the compile command.  They'll be found and pulled in automatically.

Chapel provides a large set of modules.  In CHPL_HOME/modules/internal you'll find those used to implement the language, and in CHPL_HOME/modules/standard the standard libraries.  Each source file in the library has its documentation embedded in it, and an HTML version has been extracted and can be found on the Chapel home site at

    http://chapel.cray.com/docs/latest/chpl-modindex.html

You can also do 'make docs' from the top directory when compiling the distribution to generate a local copy in CHPL_HOME/modules/docs/index.html.

Any Chapel program automatically uses four of the standard modules: the Base library (which is documented in the language spec), IO, Math, and Types.  You do not have to import these yourself.  It's in the standard libraries where we'll find two others we'll want to use, Help.chpl and Error.chpl.  

Our first task is to sanity-check the config constants.  Since 0 is a valid pixel address (x and y are 0-based), we need to change the default initial value to something less than 0.  We then test that the user has supplied a non-negative value on the command line.  We will also check that there are input and output file names; the default initial value of an empty string will work fine.  We will use the C function PNG_isa() to verify that the input file is a PNG file.  Once the file has been read, we can then check that x and y are not too big.  Because the values are 0-based, this means they must be less than the number of columns or rows.

The if statement has two forms.  We can follow the conditional test with the keyword then and a single command, possibly followed by else and a single command.  Or we can use braces for multiple commands.  If we use the keyword form and nested if's, the else will bind to the last if.

    const a = 1;

    const b = 2;

    const c = 1;

    if (a == b) then

      writeln("a matches b");

    writeln("going to next test");

    if (a == c) then

      writeln("a matches c");

    else

      writeln("a, c, differ");

    writeln("tests done");

    > going to next test

    > a matches c

    > tests done

    if (a == b) {

      writeln("a matches b");

      writeln("going to next test");

    }

    if (a == c) {

      writeln("a matches c");

    } else {

      writeln("a, c differ");

      writeln("tests done");

    }

    > a matches c

    if (a == b) then

      writeln("a matches b");

    else if (a != c) then

      writeln("a, c, differ");

      else

        writeln("a matches c");

    > a matches c

(The example is in ex_if.chpl.  Compile and run it with

    chpl -o bin/ex_if

or  make ex_if

    bin/ex_if

)

What do we do if we have a bad value?  The Help module provides one method, printUsage(), that creates the usual output for the -h argument to the executable.  It prints a table with the standard command line arguments and configuration variables.  It does not exit, however.  We'll create our own usage procedure that prints an error message, then the normal help output, and then exits.

    config const x : c_int = -1;

    config const y : c_int = -1;

 

    proc usage(msg : string) {

      writeln("\nERROR");

      writeln("  ", msg);

      printUsage();

      exit(1);

    }

 

    if (x < 0) then

      usage("missing --x or value < 0");

    if (y < 0) then

      usage("missing --y or value < 0");

    if ("" == inname) then

      usage("missing --inname");

    if (!PNG_isa(inname)) then

      usage("input file not a PNG picture");

    if ("" == outname) then

      usage("missing --outname");

    PNG_read(inname, rgb);

 

    if (rgb.ncol <= x) then

      usage("--x (0-based) >= image width " + rgb.ncol);

    if (rgb.nrow <= y) then

      usage("--y (0-based) >= image height " + rgb.nrow);

Note that we're using the integer interpretation of a boolean, ie. zero is false and everything else is true, for the test on PNG_isa().  The + operator applied to string concatenates them.  In the last two tests the ncol/nrow integer is converted to a string representation and combined with the error messages.

Our second goal is to improve error handling.  In C our house style is to have a function return a negative value as an error code.  Each function call must be followed by a test of the return value.  If there is no local storage to clean up, then we can return the error code, chaining all the back to main().  If there is an allocation, we define a label cleanup: at the end of the function, followed by freeing any non-NULL pointers.  In this case a negative return value causes a jump to cleanup:.  The macros RETONERR and CLEANUPONERR concisely take care of the error handling; you can see them in use in PNG_read() in img_png.c, or main() in test_png.c.

Chapel does not allow this, nor does it offer some form of try..catch..finally.  Looking at the IO module, the preferred style seems to be to have an optional error argument.  If not provided, the program can call halt() (a procedure defined in the Base module) after printing an error message. Otherwise it will return a value in the argument.  To make an argument optional, the module defines two versions of the function and uses overloading to allow the user to pick one.

As an example, the IO module has two variations of file.close():

    proc file.close() {

      var err:syserr = ENOERR;

      this.close(err);

      if err then ioerr(err, "in file.close", this.tryGetPath());

    }

    proc file.close(out error:syserr) {

      check();

      on this.home {

        error = qio_file_sync(_file_internal);

      }

    }

There's many things in this snippet that we haven't covered yet, but the basic flow should be clear.  The variation without the error argument calls the variation with (this.close(err)), internally passing the error result to the procedure ioerror() which is defined in Error.chpl.  ioerror() constructs the error message from its parts (here the error code, a piece of text, and the path to the file that caused the error) and calls an internal function to print it and exit.

This way of handling errors doesn't really help us, though.  The best we can do is test the return value from the PNG functions and manually clean-up and halt if there's a problem.  Since we won't use the C interface much, though, this is sufficient.  For example,

    var retval : int;

    retval = PNG_write(outname, rgb);

    if (retval < 0) {

      delete rgb;

      halt(“program stopped after error”);

    }

The new version of the program can be found in rw_png_v3.chpl.  Compile it with

    chpl -o bin/rw_png_v3 rw_png_v3.chpl img_png_v2.h build/img_png_v2.o -lpng

or  make rw_png_v3

    bin/rw_png_v3 --inname=bear.png --outname=tstimg.png --x=615 --y=212

Try different values for the arguments to see if they are handled correctly.  To test the error handling, you can run the program once and turn off write permission to the output file before running it a second time, which will cause PNG_write() to fail.

Aside: Variadic Procedures (rw_png_v3b.chpl)

Although Chapel doesn't have macros, it does have generic procedures and compile-time parameters that let us cleanly handle errors.  Consider the following,

    proc cleanup_onerr(retval : int, inst ...?narg) : void {

      /* Not an error, so do nothing. */

      if (0 <= retval) then return;

      for param i in 1..narg {

        /* Note we skip the argument if we don't know how to clean it up. */

        if isClass(inst(i)) then delete inst(i);

        /*

        else if (<type> == inst(i).type) then cleanup_type(inst(i));

        */

      }

      writeln("  exiting with error code " + retval);

      exit(retval);

    }

This uses a lot of the langauge we haven't seen yet.  The ...?narg notation denotes a variadic argument list of mixed type (so this is a generic procedure).  narg is the number of arguments in the list and is a parameter.  The ? is a query that assigns a value to narg when the compiler knows how many arguments there are from the caller.  for param is a parameter loop that is unrolled at compile time, with the body repeated for each value in the range 1..narg.  inst(i) accesses the i'th argument.  isClass() is a procedure in the standard module Types.chpl that returns true if the argument is an instance of a class.  So, if the i'th argument is an instance, we delete it.  Other types could be handled by adding clauses.  When done it prints a message to let the user know the program is going down, and exits.

All this is done at compile time.  If you wanted to limit the list to arguments of just one type, put the type name before the ellipsis.

    proc joinbyline(x : string...?)

You can also force the number of arguments by changing the '?' to a number.  This might not seem useful – why not just list that many arguments? – but consider that the number could be generated from a parameter and therefore set at compile time.

rw_png_v3b.chpl shows this procedure in use after the PNG_read() and PNG_write() calls.  For simplicity we'll stay with the approach in _v3 for the rest of this exercise, but we'll be using the _v3b procedure in the other sections.  Compile the program with

    chpl -o bin/rw_png_v3b rw_png_v3b.chpl img_png_v2.h build/img_png_v2.o -lpng

or  make rw_png_v3b

    bin/rw_png_v3b --inname=bear.png --outname=tstimg.png --x=615 --y=212

Aside: Main / Program Organization (rw_png_v4.chpl)

One final small question to answer.  Passing named options on the command line adds a good bit of typing.  Is there a way to parse the command line?  In C we get the options as strings passed to main:

    int main(int argc, char **argv) { .. }

but our programs don't have a main() function.

Well, they do, but's an empty procedure automatically generated by the compiler.  

    proc main() { }

Chapel starts a program by running all top-level statements in a module.  Initialization begins in the module that defines main(), then those it uses (but only once, so there cannot be multiple includes) in the order given in the program.  The example CHPL_HOME/examples/spec/Modules/init-order.chpl shows how this works:

    module M1 {

      use M2.M3;

      use M2;

      writeln("In M1's initializer");

      proc main() {

        writeln("In main");

      }

    }

    module M2 {

      use M4;

      writeln("In M2's initializer");

      module M3 {

        writeln("In M3's initializer");

      }

    }

    module M4 {

      writeln("In M4's initializer");

    }

    > In M4's initializer

    > In M2's initialiser

    > In M3's initializer

    > In M1's initializer

    > In main

M1 contains the main function, and begins with the 'use M2.M3;' statement. To initialize M3, we need to have initialized M2, which requires setting up M4.  Therefore, M4 is first, then M2 finishes its initialization before M3. M2 has already been set up when we reach the 'use M2;' statement in M1, so we skip it and finish M1.  Finally the program starts executing main.

A program with multiple main procedures may fail to compile, depending on the implementation.  The compiler option --main-module <module> lets you select one.

Chapel does also support a main with one argument:

    proc main(args: [] string) {

      for a in args {

        /* do work here */

      }

    }

This uses two language features that we'll talk about in the next exercise. [] indicates the variable is an array, and for a in args loops over all elements of args, assigning each in turn to the variable a.  Configuration variables and built-in options that have already been processed are not passed to main().  You will not see them in the list.  The exception is -h and --help, which are passed, with the goal being to allow the program to provide a different help message or to supplement the standard text produced by the Help module.  As with C, the first argument will be the program name.

In rw_png_v4.chpl you'll find an example of using main() to simulate the argument naming rules for procedures on the command line.  That is, with these edits command line options can either be given by their flag (--inname=<file>) or positionally.  The positional arguments work from left to right, skipping those that have been provided.  So without any named options the program will take four values on the command line,

    rw_png_v4 <inname> <outname> <x> <y>

(the same order as for test_png).  If --x were given, then three would be needed:

    rw_png_v4 --x=<x> <inname> <outname> <y>

The changes needed to implement this were to make the four variables var instead of const (so we can change them), wrapping all the code outside the variable and procedure declarations in main(), and adding this loop:

    for a in args[1..] {

      if (("-h" == a) || ("--help" == a)) {

        printUsage();

        exit(0);

      }

 

      if ("" == inname) then inname = a;

      else if ("" == outname) then outname = a;

      else if (x < 0) then x = a : c_int;

      else if (y < 0) then y = a : c_int;

      else usage("too many arguments on command line");

    }

There's a couple features in this code that we haven't seen yet.  args[1..] represents a slice – a subrange of the array.  It starts at index 1 and goes on until the array runs out of elements.  We need to skip the first element, the program name.  The second thing is the presence of a type when x or y are set, as in x = a : c_int. The type here represents a cast, and uses built-in converters from strings to the numeric types.  We don't need an equivalent to C's sscanf(). So for each argument in the array we test which configuration variable has not yet been set in the order arguments should appear on the command line.  The variable will have already been set if it was provided by a command line option.

As usual, compile and run this with

    chpl -o bin/rw_png_v4 rw_png_v4.chpl img_png_v2.h build/img_png_v2.o -lpng

or  make rw_png_v4

    bin/rw_png_v4 --inname=bear.png --outname=tstimg.png --x=615 --y=212

This is not a recommendation for using main() to parse the command line!  The purpose of this section was to talk about the overall program organization and the start-up sequence.  We will not use this style again, but will always employ command-line options.  If typing the options gets tiresome you can instead put them in a file, as we'll see in the Gabor Filter section.

 

One other note that hasn't fit elsewhere. Chapel supports both /* and */ and // style comments.  /* */ nest, so

    /* this is

      /* a wrapped comment */

      wrapper */

is legal.

Wrap-Up

We've used the need to use a C library to read and write images to cover Chapel's variables and procedures, the basic data structures, and program organization.  We've learned:

Now that we can read in images, it's time to actually some parallel image processing.  We'll start with a color conversion program.

Exercises

1. We're writing a greyscale image as a color image, setting all three color planes to the same value.  PNG does have a PNG_COLOR_TYPE_GRAY that takes only a single byte instead of three, saving space.  Modify PNG_write() to take an extra argument whether to save all three color planes or just one (specifying which plane to save).  (You'll find our modification in img_png_v3 and rw_png_v5.  We'll be using this version for the color converter.  If you define an enum in C to specify the color planes, then its members can be imported one by one into Chapel by declaring them extern const.)

2. Add a cropping function in C that takes one rgbimage and the corners of a cropping rectangle, or one corner and a width and height, and creates a new image with just the pixels inside.  Create a Chapel program that reads an image, crops it, and writes the smaller image back out.

Files

A tarball with the programs and image for this section is available here.  A zip file is here.

A PDF copy of this text is here.