st_atim and st_atimespec

2025-03-06, Thu

I was testing the following code snippet from a book1 today:

struct stat statbuf;
struct timespec times[2];
if (stat("some-file-name", &statbuf) < 0) {
  fprintf(stderr, "stat error: %s\n", strerror(errno));
  exit(1);
 }
times[0] = statbuf.st_atim;
times[1] = statbuf.st_mtim;

only to get a compilation error that complains missing field st_atim:

src/figure-4-21.c:56:24: error: no member named 'st_atim' in 'struct stat'
   56 |     times[0] = statbuf.st_atim;
      |                ~~~~~~~ ^

I was confused for a moment, because st_atim is defined in POSIX.1-2017, and also enlisted in the Debian manual for stat(3type). So I checked manual of stat(2) in macOS, which states that the stat structure is defined as:

struct stat { /* when _DARWIN_FEATURE_64_BIT_INODE is NOT defined */
  dev_t    st_dev;    /* device inode resides on */
  ino_t    st_ino;    /* inode's number */
  mode_t   st_mode;   /* inode protection mode */
  nlink_t  st_nlink;  /* number of hard links to the file */
  uid_t    st_uid;    /* user-id of owner */
  gid_t    st_gid;    /* group-id of owner */
  dev_t    st_rdev;   /* device type, for special file inode */
  struct timespec st_atimespec;  /* time of last access */
  struct timespec st_mtimespec;  /* time of last data modification */
  struct timespec st_ctimespec;  /* time of last file status change */
  off_t    st_size;   /* file size, in bytes */
  quad_t   st_blocks; /* blocks allocated for file */
  u_long   st_blksize;/* optimal file sys I/O ops blocksize */
  u_long   st_flags;  /* user defined flags for file */
  u_long   st_gen;    /* file generation number */
};

Hmm, so it is st_atimespec here? Interesting. I udpated the code accordingly:

struct stat statbuf;
struct timespec times[2];
if (stat("some-file-name", &statbuf) < 0) {
  fprintf(stderr, "%s: stat error", strerror(errno));
  exit(1);
 }
times[0] = statbuf.st_atimespec;
times[1] = statbuf.st_mtimespec;

and got similar error:

src/figure-4-21.c:56:24: error: no member named 'st_atimespec' in 'struct stat'
   56 |     times[0] = statbuf.st_atimespec;
      |                ~~~~~~~ ^

What went wrong? I scratched my head and did some search. It turns out the answer might lie in the header file, which could be accessed through less $(xcrun --sdk macosx --show-sdk-path)/usr/include/sys/stat.h2. Scroll down a few pages and the definition of stat is right there:

struct stat {
        dev_t           st_dev;         /* [XSI] ID of device containing file */
        ino_t           st_ino;         /* [XSI] File serial number */
        mode_t          st_mode;        /* [XSI] Mode of file (see below) */
        nlink_t         st_nlink;       /* [XSI] Number of hard links */
        uid_t           st_uid;         /* [XSI] User ID of the file */
        gid_t           st_gid;         /* [XSI] Group ID of the file */
        dev_t           st_rdev;        /* [XSI] Device ID */
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
        struct  timespec st_atimespec;  /* time of last access */
        struct  timespec st_mtimespec;  /* time of last data modification */
        struct  timespec st_ctimespec;  /* time of last status change */
#else
        time_t          st_atime;       /* [XSI] Time of last access */
        long            st_atimensec;   /* nsec of last access */
        time_t          st_mtime;       /* [XSI] Last data modification time */
        long            st_mtimensec;   /* last data modification nsec */
        time_t          st_ctime;       /* [XSI] Time of last status change */
        long            st_ctimensec;   /* nsec of last status change */
#endif
        off_t           st_size;        /* [XSI] file size, in bytes */
        blkcnt_t        st_blocks;      /* [XSI] blocks allocated for file */
        blksize_t       st_blksize;     /* [XSI] optimal blocksize for I/O */
        __uint32_t      st_flags;       /* user defined flags for file */
        __uint32_t      st_gen;         /* file generation number */
        __int32_t       st_lspare;      /* RESERVED: DO NOT USE! */
        __int64_t       st_qspare[2];   /* RESERVED: DO NOT USE! */
};

Based on this snippet, the st_atimesepc and its siblings are guarded by macro _DARWIN_C_SOURCE. Hence in order to make things work, we better provide it either in the source file, or through GCC option, e.g.

figure-4-21: src/figure-4-21.c ${OTHER_FILES}
        ${CC} -g -D _DARWIN_C_SOURCE -o out/$@ $^

which indeed solves the problem.

Footnotes:

1

Advanced Unix Programming Environment, http://www.apuebook.com/. The code snippet comes from Figure 4.21.

2

The include path is also enlisted in the verbose output of GCC.