tags:

views:

340

answers:

2

I'm writing my first major C API, and I want to get things right. The library allocates and frees memory for an internal struct - which is hidden from the client using a typedef. The rough structure of the data I'm providing access to is this:

disc->program->track

Associated with the disc are things like file descriptor to read from, size of the file, number of programs, and some disc-wide properties.

Associated with the programs are things like (program) index into the disc, physical offset into the file, number of tracks and a name.

Associated with the tracks are things like (track) index into the program, name, and a bunch of offsets into the file.

Each structure has a pointer to the parent structure.

I have a few questions, but I'll try to keep it brief:

  • Should the track/program know which index it's at in the parent structure?
  • This hierarchy of structures seems rather complex, but the alternative is passing the program and/or track index to various functions. Which is preferable?
  • Any more general comments?

This is a filesystem-like structure (but it's read only), I would like it to be compatible with multithreading in the future, and it needs to be portable. I am specifically talking about C here - no C++.

+2  A: 

Heavily Edited Intro I initially criticised the problem description as confusing. My problems stem from way the concept of a 'file' is used in the question. The question implies the Disc, Programs and Tracks are all stored in one 'file'. I thought the questioner was building his own filesystem, which would make this 'everything in one file' structure weird, but I have now decided he's probably not doing that, in which case it's less weird. So I'll go ahead and offer a real answer, based on the assumption he's using an existing (probably standard) file system, and his entire data structure is stored in one file within that file system. If I am assuming wrong no doubt I'll be corrected.

I will first offer one piece of general advice for situations like this; Look at things from the perspective of the API user first. Then design your API so that the code he will write flows easily with no need to deal with the details that are properly in your domain.

One way to work on your API design is to write some user code first and define the API so that that code is easy to write. As a bonus, after you actually implement the API you'll then have some test code to try it out with.

Moving on to more specific advice;

Here is a catalog of the three data types in the system. We can treat them as abstract data types or 'Objects' if you like and define a typdef'ed struct (DISC, PROGRAM, TRACK say) to represent each one.

disc = a collection of programs stored in a file
+-----------+
|file       |
+-----------+
|program    |
+-----------+
|program    |
+-----------+
|...        |
+-----------+
|program    |
+-----------+

program = a collection of tracks
+-----------+
|ptr->disc  |
+-----------+
|name       |
+-----------+
|file offset|
+-----------+
|track      |
+-----------+
|track      |
+-----------+
|...        |
+-----------+
|track      |
+-----------+

track = a collection of audio samples
+------------------+
|ptr->program      |
+------------------+
|name              |
+------------------+
|file offset+length|
+------------------+
|file offset+length|
+------------------+
|...               |
+------------------+
|file offset+length|
+------------------+

I would propose that you do not make your users pick out data from the structures. You can't really hide the internals of the structures in C (without jumping through hoops with casting etc.), but you can provde a family of functions that let's your users do what they need to do without accessing the contents of the abstract types themselves. For example our family of functions might look like this;

// DISC functions
DISC      *dopen(  const char *disc_name );
void       dstats( int *ptr_nbr_programs, FILE **ptr_file );
void       dclose( DISC *disc );

// PROGRAM functions
PROGRAM   *popen_name( DISC *disc, const char *program_name );
PROGRAM   *popen_idx ( DISC *disc, int program_idx );
void       pstats( int *ptr_nbr_tracks );
void       pclose( PROGRAM *program );

// TRACK functions
TRACK     *topen_name( PROGRAM *program, const char *track_name );
TRACK     *topen_idx ( PROGRAM *program, int track_idx );
int        tread( unsigned char *buf, int nbytes_to_read );
void       tseek( unsigned long offset );
void       tclose( TRACK *track );

This should all be reasonably self explanantory - it is modelled on the existing standard C FILE paradigm.

First your user obtains a ptr to DISC with dopen(). Assuming this works (it will return NULL if it doesn't), he can obtain any global DISC information with dstats(). More to the point he can obtain a ptr to a PROGAM within the DISC with one of the popen() family of functions.

With a ptr to PROGRAM he can drill down further and obtain a single TRACK with one of the topen() family of functions.

A very important point is that you don't make your user iterate through the audio fragments himself to get data from the TRACK. The user provides a buffer to read the samples into, and you iterate as necessary through the fragments to fill that buffer. The tseek() function is provided to give him random access.

I haven't tried to work out the details of every parameter and how errors are handled etc., I am just presenting a concept to be refined.

Note that the 'sandwich' paradigm is used throughout. An introductory 'open' and a concluding 'close' sandwiches operations on every type.

Bill Forster
It's an API to interact with a filesystem for a multitrack recorder. Disk means hard disk, Program means a multi-track recording and track means a single track of audio, and each track has a number of (offset, count) pairs pointing to fragmented bits of audio. Does that clarify things at all?
Draemon
I'm getting there :-)>Associated with the disc are things like file descriptor to read from, size of the file, number of programs, and some disc-wide properties.Why does the disc have a file descriptor to read from? Is this your own kind of file descriptor or a normal (i.e. stdio.h) file descriptor ?
Bill Forster
It seems the author of the question has moved on and is not interested in further discussion. A little disappointing after putting in some thought and some work.
Bill Forster
A: 
  • Should the track/program know which index it's at in the parent structure?

Offhand I don't suppose there's any reason to rule against this. It's hard to say without examples...

  • This hierarchy of structures seems rather complex, but the alternative is passing the program and/or track index to various functions. Which is preferable?

Can you give examples of each? Even a couple of function declarations may help clear it up.

  • Any more general comments?

If it's really a read-only structure, you don't need to be too concerned about multithreading in the future.

Dan Olson