What's a good way to be hardware independent?

Here is another take on the same thing.

gcc-avr supports bit fields. This allows value to be directly assigned to a bit.

typedef struct {
	unsigned char B0: 1;
	unsigned char B1: 1;
	unsigned char B2: 1;
	unsigned char B3: 1;
	unsigned char B4: 1;
	unsigned char B5: 1;
	unsigned char B6: 1;
	unsigned char B7: 1;
} BIT8_T;

//extend bit fields
#define PORTBbits	(*(volatile BIT8_T *)&PORTB)
#define DDRBbits	(*(volatile BIT8_T *)&DDRB)

//helper functions. don't call directly
#define _DDR(id, pin)			DDR ## id ## bits.B ## pin
#define _PORT(id, pin)			PORT ## id ## bits.B ## pin

//port functions
#define DDR(pin)				_DDR(pin)
#define PORT(pin)				_PORT(pin)

PORTBbits and DDRBbits are bit fields of their whole byte countparts - you can expand them easily to cover other registers, like PINx.

DDR() and PORT() functions are actually what we want. _DDR() and _PORT() are there to defeat the compiler so don't use them.

If you define a pin like this:

#define HC595_SCK B, 0 //hc595_sck on portb.0

You can later directly assign values to it, like this:

#define HC595_SCK B, 0 //hc595_sck on portb.0

  DDR(HC595_SCK) = 1; //hc595_sck as output

  do {
    PORT(HC595_SCK) = 0; //clear sck
    ...
    PORT(HC595_SCK) = 1; //set sck
  }

...
PORT(HC595_SCK) ^= 1; //toggle hc595_sck

To some, this may be more intuitive. But it is slightly slower, due to the bit fields.