High and low-level C
High-level and underlying C
Jim Larson
1996-09-13
This talk was given at the Section 312 programming lunchtime seminar.
Introduction
Introduction
Tower of computing ages. High-level computing ages can (mostly) Compile to lower-level ones.
In the language Tower, almost all high-level languages can be compiled into the underlying language.
Might want to write in low-level language for access to hardware level. Can write high-level code in low-level language by compiling in your head.
You may want to write the underlying language for accessing the hardware layer. By compiling in your mind, you can write high-level languages in the underlying language.
Might want to write in High-level language for flexibility and power. Can write low-level code in High-level language by "writing through the compiler ".
You may want to write high-level languages with flexibility and strength. By using compilers, you can write the underlying language in high-level languages.
C features
C features
Recursive functions
Recursive functions
C has a stack used for the call stack, activation records, and local variables.
Note that functions are not nested, as in Pascal. This affords greater freedoms for using function pointers.
C has a stack for calling, activity recording, and local variables. Note that functions cannot be nested in Pascal. Function pointers provide greater freedom.
/*
* Simple example of a recursive function.
*/
Unsigned
FIB (unsigned N)
{
If (n = 0 | n = 1)
Return 1;
Else
Return fib (n-1) + fib (n-2 );
}
This simple example is a little contrived, as well as a lousy way to compute Fibonacci numbers. A better example shows the natural relation between recursive functions and recursive data structures-those that have references to other objects of the same type.
As a simple example of poor computation of the Fibonacci series, the Code means it. A better example shows the natural relationship between recursive functions and recursive data structures (objects that reference other data structures of the same type ).
/*
* Recursive functions work well with recursive data structures.
* Recursive functions work well with recursive Data Structures
*/
Typedef struct expr;
Struct expr {
Enum {number, plus, minus, times, divide} op;
Union {
Double number;
Struct {
Expr * left, * right;
} Child;
} U;
};
Double
Eval (expr * E)
{
Switch (e-> OP ){
Case number: Return e-> U. number;
Case plus: Return eval (e-> U. Child. Left) + eval (e-> U. Child. Right );
Case minus: Return eval (e-> U. Child. Left)-eval (e-> U. Child. Right );
Case times: Return eval (e-> U. Child. Left) * eval (e-> U. Child. Right );
Case divide: Return eval (e-> U. Child. Left)/eval (e-> U. Child. Right );
}
}
Dynamic Memory Allocation
Dynamic Memory Allocation
Stack-allocation-local variables.
Static Memory Allocation-local variables
Heap-allocation. Library only, but pervasively used. (actually, this is our first example of a high-level feature implemented entirely in Standard C .)
Heap allocation, used only for databases, but is widely used (in fact, this is our first high-level implementation example, fully implemented using standard C)
ABSTRACT Data Types
ABSTRACT Data Type
C type theory is kind of confusing: Types where you know the size of the object types where you don't know the size of the object void *
Tricks: You can declare and use a pointer to an incomplete type. you can complete an incomplete type later. pointers to structures can be coerced to and from pointers to their first element, if it also is a structure.
The C-type theory is a obfuscation: You know the size of the object type, but you do not know the size of the void * object type.
Tip: You can declare a pointer of an incomplete type and then complete the type. The pointer pointing to the structure and the pointer pointing to the first element (if it is also a structure) can be forcibly converted.
/* In widget. H */
Typedef struct widget;
Extern widget * widget_new (double length, Enum color );
Extern double widget_mass (widget * w );
Extern int widget_fitsin (widget * w_inner, widget * w_outer );
Extern void widget_delete (widget * w );
The implementation gets to hide information about the representation, as well as guarantee invariants.
/* In widget. C */
# Include <stdlib. h>
# Include "Colors. H"
# Include "widget. H"
/*
* Non-Public definition of widget structure, declared in "widget. H ".
*/
Struct widget {
Widget * Next;/* widgets are stored on a linked list */
Int ID;/* identification stamp */
Double Length;/* length in centimeters */
Double Mass;/* Mass in grams */
Enum color;/* See "Colors. H" for definitions */
};
Static const double widget_height = 2.54;/* in centimeters */
Static const double widget_density = 1.435;/* in g/cm ^ 3 */
Static widget * widget_list = 0;
Static int widget_next_id = 0;
/*
* Create a new widget. Calculate widget mass. Keep track
* Bookkeeping with ID number and store it on linked list of widgets.
*/
Widget *
Widget_new (double length, Enum color)
{
Widget * w = malloc (sizeof (widget ));
If (! W)
Return 0;
W-> next = widget_list;
Widget_list = W;
W-> id = widget_next_id ++;
W-> length = length;
W-> mass = 0.5 * length * widget_height * widget_density;
W-> color = color;
Return W;
}
Nonlocal exits
Non-local presence
Setjmp/longjmp work like a bunch of immediate returns from functions. Intermediate functions don't need to make provisions for this-modular way to raise error conditions.
Viewing function call/Return Sequences (aka procedure activations) as a tree, longjump can only work on a saved jmp_buf from a parent in the tree.
Setjmp/longjmp works like returning a series of functions. Intermediate functions do not need to handle modular methods to throw error conditions.
# Include <signal. h>
# Include <setjmp. h>
Static jmp_buf begin;
Static void
Fpecatch (INT sig)
{
Warning ("floating point exception ");
Longjmp (begin, 0 );
}
Void
Command_loop (void)
{
For (;;){
If (setjmp (BEGIN )){
Printf ("command failed to execute! /N ");
}
Signal (sigfpe, & fpecatch );
Prompt ();
Do_command (read_command ());
}
}
High-Level C
High-level C
Classes and objects
Class and Object
The core of OO is "dynamic dispatch"-you don't know which function you're calling. function pointers also provide this kind of indirection.
Structure coercion and nesting provide for single-inheritance of classes.
The core of OO is "Dynamic Allocation"-you don't know which function you are calling. Function pointers also provide this indirect property.
Method callcan be masked by functions or macros. Can "crack open" the specified action to cache methods.
Method calls can be concealed by functions or macros. It can be abstracted from "hacker-type open" to the cache method.
/* In shape. H */
Typedef struct point;
Struct point {
Double X, Y;
};
Typedef struct shape;
Struct shape {
Void (* move) (shape * Self, point P );
Void (* scale) (shape * Self, double factor );
Void (* rotate) (shape * Self, double degrees );
Void (* redraw) (shape * Self );
};
Extern shape * make_triangle (point center, double size );
In the implementation:
In implementation:
/* In triangle. C */
# Include <stdlib. h>
# Include "shape. H"
Typedef struct triangle;
Struct triangle {
Shape OPS;
Point center;
Point voffset [3];
};
Static void
Tri_move (shape * Self, point P)
{
Triangle * t = (Triangle *) self;
T-> center = P;
}
Static void
Tri_scale (shape * Self, double factor)
{
Triangle * t = (Triangle *) self;
Int I;
For (I = 0; I <3; ++ I ){
T-> voffset [I]. x * = factor;
T-> voffset [I]. y * = factor;
}
}
Static void
Tri_redraw (shape * Self)
{
Triangle * t = (Triangle *) self;
Point c = T-> center;
Point V0 = addpoint (C, T-> voffset [0]);
Point V1 = addpoint (C, T-> voffset [1]);
Point V2 = addpoint (C, T-> voffset [2]);
Drawline (v0, V1 );
Drawline (V1, V2 );
Drawline (V2, V0 );
}
Shape triangle_ops = {& tri_move, & tri_redraw, & tri_scale, & tri_rotate };
Shape *
Make_triangle (point center, double size)
{
Triangle * t = malloc (sizeof (triangle ));
If (! T)
Return 0;
T-> Ops = triangle_ops;
T-> center = center;
T-> voffset [0]. x = size * v0x;
T-> voffset [0]. Y = size * v0y;
T-> voffset [1]. x = size * v1x;
T-> voffset [1]. Y = size * v1y;
T-> voffset [2]. x = size * v2x;
T-> voffset [2]. Y = size * v2y;
Return & T-> OPS;
}
In a client module that uses the interface:
The client module that uses this interface:
/* In animate. C */
Void
Pulsate (shape * s, double period)
{
Double factor = 1.0, step = 0.1;
Int I;
Void (* scale) (shape *, double) = S-> scale;/* cache Method */
Void (* redraw) (shape *) = S-> redraw;
For (;;){
For (I = 0; I <10; ++ I ){
Factor + = step;
(* Scale) (S, factor );
(* Redraw) (s );
Sleep (1 );
}
Step * =-1.0;
}
}
Closures
Closure
In scheme, abstractions carry their environment with them. this is like bundling a function pointer and some data to work. the data acts like "configuration" data. can either make it an explicit argument, or create ADT for closure and make "apply_closure" function-breaks from ordinary function syntax.
Much like objects, implemented above, but less constrained in use.
In scheme, abstraction carries their own environment, just like function pointers and related data. The data can be an explicit parameter like the configuration data, or an ADT is created from the closure, and the apply_closure function is used, although this breaks the regular function syntax.
/* In closure. H */
Typedef struct closure;
Struct closure {
Void * (* fN) (void *);
Void * data;
}
Inline void *
Appclosure (Closure * t)
{
Return (* t-> FN) (t-> data );
}
Exception Handling
Exception Handling
Want to be able to raise a certain kind of error to by handled by a designated handler, but with the "linkage" established dynamically.
If you want to be able to raise a specific type of error through a set exception, you can dynamically link it (it is difficult to translate this sentence)
# Include "exception. H"
Void
Logcommands (char * filename)
{
If (! (F = fopen (filename )))
Throw_error (err_filename );
Catch_error (all_errs ){
Fflush (f );
Fclose (f );
Throw_error (throwerror );
} Errors_in {
While (x = read_input (stdin ))! = EOF ){
A = compute_result (X );
Print_result (A, F );
}
} End_error;
}
The implementation is kind of tricky-Use of ternary expression to make complicated series of tests into a syntactic expression:
This implementation is a trick-using a ternary expression to create a test of a complex sequence into a syntax expression
/* In exception. H */
Const int maxcatchers' = 100;
Extern jmp_buf catchers [maxcatchers];
Extern volatile int nextcatch;
Extern volatile int throwerror;
# Define all_errs 0
# Define catch_error (e )/
If (nextcatch = maxcatchers )/
? Error ("too handle exception handlers ")/
: (Setjmp (catchers [nextcatch ++]) = 0 )/
? 0/
: (E )! = All_errs & throwerror! = (E ))/
? Longjmp (catchers [-- nextcatch])/
: 1)
# Define errors_in else
# Define end_error do {-- nextcatch;} while (0)
# Define throw_error (e )/
Do {/
Throwerr = (E );/
Longjmp (catchers [-- nextcatch]);/
} While (0)
Continuations
Coroutine
Scheme's general continuations will let you resume at any previous location in call tree-cool for escapes, coroutines, backtracking, etc.
Setjmp/longjmp will let you jump up to a known location. Disciplined use can result in catch/throw.
Scheme's general coroutine allows you to restore the call tree to the previous position-for escape, coroutine and direction tracking are cool.
By making stacks explicit, you can do coroutines.
By making the stack explicit, You can implement coroutine
By using a continuation-Passing Style, you can do arbitrarily cool things using Cheney on the MTA. See paper by Henry Baker.
By using the coroutine transfer style, you can use Cheney on the MTA to do anything cool. See Henry Baker's paper.
Typedef void * (* genfun) (void );
Void
Trampoline (genfun F)
{
While (f)
F = (genfun) (* f )();
}
Garbage-collected memory
Garbage collection memory
Through explicit maintenance of roots into the GC 'd heap, and the types of objects, you can safely ignore free () and the associate bookkeeping.
"Conservative GC" implementations allow GC behavior for standard programs. See work by Hans Boehm, or the specified cial product by geodesic systems.
By explicitly maintaining the root and object types in the GC heap, you can safely ignore free and related bookkeeping work.
Cheney on the MTA gives simple generational capabilities.
The Cheney on MTA provides simple model capabilities.
Low-level C
Underlying C
Bounded memory usage
Bounded memory usage
Ted drain's idea: Have a malloc () substitute profile allocation for a sample run, then can build a dedicated Allocator for a heap whose size is known at compile-time.
The idea of Ted drain: for simple operations, create a malloc replacement profiling allocation, and then create a dedicated distributor for the heap. The heap size is known during compilation.
# Include <assert. h>
# Include <stdio. h>
# Include <stdlib. h>
Static size_t allocbytes = 0;
Void
Printalloc (void)
{
Fprintf (stderr, "allocated % lu Bytes/N", (unsigned long) allocbytes );
}
Void
Ememinit (void)
{
Int err;
Err = atexit (& printalloc );
Assert (ERR = 0 );
}
Void *
Emalloc (size_t size)
{
Void * P = malloc (size );
Assert (P );
Allocbytes + = size;
Return P;
}
Bounded call stack depth
Bounded call stack depth
Trampoline back and manage stack explicitly.
Can also implement a State-machine within a function.
An explicit management stack is returned from a conventional method, and a state machine can be implemented in a function.
Data Type Representation
Data Type Representation
Structure Ordering and offsets-make explicit with arrays.
Byte-level representation-use arrays of characters. Portable binary representations of output.
Structural order and offset-use an array for explicit implementation.
Direct hierarchical representation-use the array feature to output portable binary representation
Typedef char limit 4hdr [20];
Struct {
Char * fieldname;
Int byteoffset;
Int bitoffset;
Int bitlength;
} Ipv4fields [] = {
{"Vers", 0, 0, 4 },
{"Hlen", 0, 4, 4 },
{"Service type", 1, 0, 8 },
{"Total length", 2, 0, 16 },
{"Identification", 4, 0, 16 },
{"Flags", 6, 0, 3 },
{"Fragment offset", 6, 3, 13 },
{"Time to live", 8, 0, 8 },
{"Protocol", 9, 0, 8 },
{"Header checksum", 10, 0, 16 },
{"Source IP address", 12, 0, 32 },
{"Desination IP Address", 16, 0, 32}
};