Commit 5b336293 authored by J. R. Okajima's avatar J. R. Okajima
Browse files

aufs: DIO and dynamically customize address_space_operations



As a result of branch management, the virtual inode may point a
different real inode from it used to. And aufs has to maintain its
address_space_operations, since its definition may affect the
behaviour.
I know some people (including grsec-patch) doesn't like a non-const
address_space_operations, but in order to keep the consistency of the
behaviour, the correct address_space_operations is important.

See also the document in this commit.
Signed-off-by: default avatarJ. R. Okajima <hooanon05g@gmail.com>
parent 27c33497
# Copyright (C) 2010-2019 Junjiro R. Okajima
Dynamically customizable FS operations
----------------------------------------------------------------------
Generally FS operations (struct inode_operations, struct
address_space_operations, struct file_operations, etc.) are defined as
"static const", but it never means that FS have only one set of
operation. Some FS have multiple sets of them. For instance, ext2 has
three sets, one for XIP, for NOBH, and for normal.
Since aufs overrides and redirects these operations, sometimes aufs has
to change its behaviour according to the branch FS type. More importantly
VFS acts differently if a function (member in the struct) is set or
not. It means aufs should have several sets of operations and select one
among them according to the branch FS definition.
In order to solve this problem and not to affect the behaviour of VFS,
aufs defines these operations dynamically. For instance, aufs defines
dummy direct_IO function for struct address_space_operations, but it may
not be set to the address_space_operations actually. When the branch FS
doesn't have it, aufs doesn't set it to its address_space_operations
while the function definition itself is still alive. So the behaviour
itself will not change, and it will return an error when direct_IO is
not set.
The lifetime of these dynamically generated operation object is
maintained by aufs branch object. When the branch is removed from aufs,
the reference counter of the object is decremented. When it reaches
zero, the dynamically generated operation object will be freed.
This approach is designed to support AIO (io_submit), Direct I/O and
XIP (DAX) mainly.
Currently this approach is applied to address_space_operations for
regular files only.
......@@ -14,6 +14,7 @@ aufs-y := module.o sbinfo.o super.o branch.o xino.o sysaufs.o opts.o \
wkq.o vfsub.o dcsub.o \
cpup.o whout.o wbr_policy.o \
dinfo.o dentry.o \
dynop.o \
file.o \
dir.o \
iinfo.o inode.o i_op.o
......
......@@ -27,6 +27,7 @@
#include "dcsub.h"
#include "dentry.h"
#include "dir.h"
#include "dynop.h"
#include "file.h"
#include "fstype.h"
#include "hbl.h"
......
......@@ -17,6 +17,7 @@ static void au_br_do_free(struct au_branch *br)
{
int i;
struct au_wbr *wbr;
struct au_dykey **key;
au_xino_put(br);
......@@ -33,6 +34,13 @@ static void au_br_do_free(struct au_branch *br)
AuRwDestroy(&wbr->wbr_wh_rwsem);
}
key = br->br_dykey;
for (i = 0; i < AuBrDynOp; i++, key++)
if (*key)
au_dy_put(*key);
else
break;
/* recursive lock, s_umount of branch's */
/* synchronize_rcu(); */ /* why? */
lockdep_off();
......@@ -338,6 +346,7 @@ static int au_br_init(struct au_branch *br, struct super_block *sb,
err = 0;
br->br_perm = add->perm;
br->br_path = add->path; /* set first, path_get() later */
spin_lock_init(&br->br_dykey_lock);
au_lcnt_init(&br->br_nfiles, /*release*/NULL);
au_lcnt_init(&br->br_count, /*release*/NULL);
br->br_id = au_new_br_id(sb);
......
......@@ -13,6 +13,7 @@
#ifdef __KERNEL__
#include <linux/mount.h>
#include "dynop.h"
#include "lcnt.h"
#include "rwsem.h"
#include "super.h"
......@@ -55,6 +56,9 @@ struct au_wbr {
unsigned long long wbr_bytes;
};
/* ext2 has 3 types of operations at least, ext3 has 4 */
#define AuBrDynOp (AuDyLast * 4)
/* sysfs entries */
struct au_brsysfs {
char name[16];
......@@ -75,6 +79,8 @@ struct au_branch {
int br_perm;
struct path br_path;
spinlock_t br_dykey_lock;
struct au_dykey *br_dykey[AuBrDynOp];
au_lcnt_t br_nfiles; /* opened files */
au_lcnt_t br_count; /* in-use for other */
......
......@@ -835,6 +835,15 @@ static int au_cpup_single(struct au_cp_generic *cpg, struct dentry *dst_parent)
}
if (cpg->bdst < old_ibtop) {
if (S_ISREG(inode->i_mode)) {
err = au_dy_iaop(inode, cpg->bdst, dst_inode);
if (unlikely(err)) {
/* ignore an error */
/* au_pin_hdir_relock(cpg->pin); */
inode_unlock(dst_inode);
goto out_rev;
}
}
au_set_ibtop(inode, cpg->bdst);
} else
au_set_ibbot(inode, cpg->bdst);
......
......@@ -14,6 +14,7 @@
#include <linux/atomic.h>
#include <linux/module.h>
#include <linux/kallsyms.h>
#ifdef CONFIG_AUFS_DEBUG
#define AuDebugOn(a) BUG_ON(a)
......@@ -70,6 +71,13 @@ AuStubInt0(au_debug_test, void)
AuIOErr(fmt, ##__VA_ARGS__); \
} while (0)
#define AuUnsupportMsg "This operation is not supported." \
" Please report this application to aufs-users ML."
#define AuUnsupport(fmt, ...) do { \
pr_err(AuUnsupportMsg "\n" fmt, ##__VA_ARGS__); \
dump_stack(); \
} while (0)
#define AuTraceErr(e) do { \
if (unlikely((e) < 0)) \
AuDbg("err %d\n", (int)(e)); \
......@@ -128,6 +136,12 @@ void au_dbg_verify_kthread(void);
au_dpri_sb(sb); \
mutex_unlock(&au_dbg_mtx); \
} while (0)
#define AuDbgSym(addr) do { \
char sym[KSYM_SYMBOL_LEN]; \
sprint_symbol(sym, (unsigned long)addr); \
AuDbg("%s\n", sym); \
} while (0)
#else
AuStubVoid(au_dbg_verify_dinode, struct dentry *dentry)
AuStubVoid(au_dbg_verify_gen, struct dentry *parent, unsigned int sigen)
......@@ -137,6 +151,7 @@ AuStubVoid(au_dbg_verify_kthread, void)
#define AuDbgDAlias(i) do {} while (0)
#define AuDbgDentry(d) do {} while (0)
#define AuDbgSb(sb) do {} while (0)
#define AuDbgSym(addr) do {} while (0)
#endif /* CONFIG_AUFS_DEBUG */
#endif /* __KERNEL__ */
......
// SPDX-License-Identifier: GPL-2.0
/*
* Copyright (C) 2010-2019 Junjiro R. Okajima
*/
/*
* dynamically customizable operations for regular files
*/
#include "aufs.h"
#define DyPrSym(key) AuDbgSym(key->dk_op.dy_hop)
/*
* How large will these lists be?
* Usually just a few elements, 20-30 at most for each, I guess.
*/
static struct hlist_bl_head dynop[AuDyLast];
static struct au_dykey *dy_gfind_get(struct hlist_bl_head *hbl,
const void *h_op)
{
struct au_dykey *key, *tmp;
struct hlist_bl_node *pos;
key = NULL;
hlist_bl_lock(hbl);
hlist_bl_for_each_entry(tmp, pos, hbl, dk_hnode)
if (tmp->dk_op.dy_hop == h_op) {
key = tmp;
kref_get(&key->dk_kref);
break;
}
hlist_bl_unlock(hbl);
return key;
}
static struct au_dykey *dy_bradd(struct au_branch *br, struct au_dykey *key)
{
struct au_dykey **k, *found;
const void *h_op = key->dk_op.dy_hop;
int i;
found = NULL;
k = br->br_dykey;
for (i = 0; i < AuBrDynOp; i++)
if (k[i]) {
if (k[i]->dk_op.dy_hop == h_op) {
found = k[i];
break;
}
} else
break;
if (!found) {
spin_lock(&br->br_dykey_lock);
for (; i < AuBrDynOp; i++)
if (k[i]) {
if (k[i]->dk_op.dy_hop == h_op) {
found = k[i];
break;
}
} else {
k[i] = key;
break;
}
spin_unlock(&br->br_dykey_lock);
BUG_ON(i == AuBrDynOp); /* expand the array */
}
return found;
}
/* kref_get() if @key is already added */
static struct au_dykey *dy_gadd(struct hlist_bl_head *hbl, struct au_dykey *key)
{
struct au_dykey *tmp, *found;
struct hlist_bl_node *pos;
const void *h_op = key->dk_op.dy_hop;
found = NULL;
hlist_bl_lock(hbl);
hlist_bl_for_each_entry(tmp, pos, hbl, dk_hnode)
if (tmp->dk_op.dy_hop == h_op) {
kref_get(&tmp->dk_kref);
found = tmp;
break;
}
if (!found)
hlist_bl_add_head(&key->dk_hnode, hbl);
hlist_bl_unlock(hbl);
if (!found)
DyPrSym(key);
return found;
}
static void dy_free_rcu(struct rcu_head *rcu)
{
struct au_dykey *key;
key = container_of(rcu, struct au_dykey, dk_rcu);
DyPrSym(key);
au_kfree_rcu(key);
}
static void dy_free(struct kref *kref)
{
struct au_dykey *key;
struct hlist_bl_head *hbl;
key = container_of(kref, struct au_dykey, dk_kref);
hbl = dynop + key->dk_op.dy_type;
au_hbl_del(&key->dk_hnode, hbl);
call_rcu(&key->dk_rcu, dy_free_rcu);
}
void au_dy_put(struct au_dykey *key)
{
kref_put(&key->dk_kref, dy_free);
}
/* ---------------------------------------------------------------------- */
#define DyDbgSize(cnt, op) AuDebugOn(cnt != sizeof(op)/sizeof(void *))
#ifdef CONFIG_AUFS_DEBUG
#define DyDbgDeclare(cnt) unsigned int cnt = 0
#define DyDbgInc(cnt) do { cnt++; } while (0)
#else
#define DyDbgDeclare(cnt) do {} while (0)
#define DyDbgInc(cnt) do {} while (0)
#endif
#define DySet(func, dst, src, h_op, h_sb) do { \
DyDbgInc(cnt); \
if (h_op->func) { \
if (src.func) \
dst.func = src.func; \
else \
AuDbg("%s %s\n", au_sbtype(h_sb), #func); \
} \
} while (0)
#define DySetForce(func, dst, src) do { \
AuDebugOn(!src.func); \
DyDbgInc(cnt); \
dst.func = src.func; \
} while (0)
#define DySetAop(func) \
DySet(func, dyaop->da_op, aufs_aop, h_aop, h_sb)
#define DySetAopForce(func) \
DySetForce(func, dyaop->da_op, aufs_aop)
static void dy_aop(struct au_dykey *key, const void *h_op,
struct super_block *h_sb __maybe_unused)
{
struct au_dyaop *dyaop = (void *)key;
const struct address_space_operations *h_aop = h_op;
DyDbgDeclare(cnt);
AuDbg("%s\n", au_sbtype(h_sb));
DySetAop(writepage);
DySetAopForce(readpage); /* force */
DySetAop(writepages);
DySetAop(set_page_dirty);
DySetAop(readpages);
DySetAop(write_begin);
DySetAop(write_end);
DySetAop(bmap);
DySetAop(invalidatepage);
DySetAop(releasepage);
DySetAop(freepage);
/* this one will be changed according to an aufs mount option */
DySetAop(direct_IO);
DySetAop(migratepage);
DySetAop(isolate_page);
DySetAop(putback_page);
DySetAop(launder_page);
DySetAop(is_partially_uptodate);
DySetAop(is_dirty_writeback);
DySetAop(error_remove_page);
DySetAop(swap_activate);
DySetAop(swap_deactivate);
DyDbgSize(cnt, *h_aop);
}
/* ---------------------------------------------------------------------- */
static void dy_bug(struct kref *kref)
{
BUG();
}
static struct au_dykey *dy_get(struct au_dynop *op, struct au_branch *br)
{
struct au_dykey *key, *old;
struct hlist_bl_head *hbl;
struct op {
unsigned int sz;
void (*set)(struct au_dykey *key, const void *h_op,
struct super_block *h_sb __maybe_unused);
};
static const struct op a[] = {
[AuDy_AOP] = {
.sz = sizeof(struct au_dyaop),
.set = dy_aop
}
};
const struct op *p;
hbl = dynop + op->dy_type;
key = dy_gfind_get(hbl, op->dy_hop);
if (key)
goto out_add; /* success */
p = a + op->dy_type;
key = kzalloc(p->sz, GFP_NOFS);
if (unlikely(!key)) {
key = ERR_PTR(-ENOMEM);
goto out;
}
key->dk_op.dy_hop = op->dy_hop;
kref_init(&key->dk_kref);
p->set(key, op->dy_hop, au_br_sb(br));
old = dy_gadd(hbl, key);
if (old) {
au_kfree_rcu(key);
key = old;
}
out_add:
old = dy_bradd(br, key);
if (old)
/* its ref-count should never be zero here */
kref_put(&key->dk_kref, dy_bug);
out:
return key;
}
/* ---------------------------------------------------------------------- */
/*
* Aufs prohibits O_DIRECT by default even if the branch supports it.
* This behaviour is necessary to return an error from open(O_DIRECT) instead
* of the succeeding I/O. The dio mount option enables O_DIRECT and makes
* open(O_DIRECT) always succeed, but the succeeding I/O may return an error.
* See the aufs manual in detail.
*/
static void dy_adx(struct au_dyaop *dyaop, int do_dx)
{
if (!do_dx)
dyaop->da_op.direct_IO = NULL;
else
dyaop->da_op.direct_IO = aufs_aop.direct_IO;
}
static struct au_dyaop *dy_aget(struct au_branch *br,
const struct address_space_operations *h_aop,
int do_dx)
{
struct au_dyaop *dyaop;
struct au_dynop op;
op.dy_type = AuDy_AOP;
op.dy_haop = h_aop;
dyaop = (void *)dy_get(&op, br);
if (IS_ERR(dyaop))
goto out;
dy_adx(dyaop, do_dx);
out:
return dyaop;
}
int au_dy_iaop(struct inode *inode, aufs_bindex_t bindex,
struct inode *h_inode)
{
int err, do_dx;
struct super_block *sb;
struct au_branch *br;
struct au_dyaop *dyaop;
AuDebugOn(!S_ISREG(h_inode->i_mode));
IiMustWriteLock(inode);
sb = inode->i_sb;
br = au_sbr(sb, bindex);
do_dx = !!au_opt_test(au_mntflags(sb), DIO);
dyaop = dy_aget(br, h_inode->i_mapping->a_ops, do_dx);
err = PTR_ERR(dyaop);
if (IS_ERR(dyaop))
/* unnecessary to call dy_fput() */
goto out;
err = 0;
inode->i_mapping->a_ops = &dyaop->da_op;
out:
return err;
}
/*
* Is it safe to replace a_ops during the inode/file is in operation?
* Yes, I hope so.
*/
int au_dy_irefresh(struct inode *inode)
{
int err;
aufs_bindex_t btop;
struct inode *h_inode;
err = 0;
if (S_ISREG(inode->i_mode)) {
btop = au_ibtop(inode);
h_inode = au_h_iptr(inode, btop);
err = au_dy_iaop(inode, btop, h_inode);
}
return err;
}
void au_dy_arefresh(int do_dx)
{
struct hlist_bl_head *hbl;
struct hlist_bl_node *pos;
struct au_dykey *key;
hbl = dynop + AuDy_AOP;
hlist_bl_lock(hbl);
hlist_bl_for_each_entry(key, pos, hbl, dk_hnode)
dy_adx((void *)key, do_dx);
hlist_bl_unlock(hbl);
}
/* ---------------------------------------------------------------------- */
void __init au_dy_init(void)
{
int i;
/* make sure that 'struct au_dykey *' can be any type */
BUILD_BUG_ON(offsetof(struct au_dyaop, da_key));
for (i = 0; i < AuDyLast; i++)
INIT_HLIST_BL_HEAD(dynop + i);
}
void au_dy_fin(void)
{
int i;
for (i = 0; i < AuDyLast; i++)
WARN_ON(!hlist_bl_empty(dynop + i));
}
/* SPDX-License-Identifier: GPL-2.0 */
/*
* Copyright (C) 2010-2019 Junjiro R. Okajima
*/
/*
* dynamically customizable operations (for regular files only)
*/
#ifndef __AUFS_DYNOP_H__
#define __AUFS_DYNOP_H__
#ifdef __KERNEL__
#include <linux/fs.h>
#include <linux/kref.h>
enum {AuDy_AOP, AuDyLast};
struct au_dynop {
int dy_type;
union {
const void *dy_hop;
const struct address_space_operations *dy_haop;
};
};
struct au_dykey {
union {
struct hlist_bl_node dk_hnode;
struct rcu_head dk_rcu;
};
struct au_dynop dk_op;
/*
* during I am in the branch local array, kref is gotten. when the
* branch is removed, kref is put.
*/
struct kref dk_kref;
};
/* stop unioning since their sizes are very different from each other */
struct au_dyaop {
struct au_dykey da_key;
struct address_space_operations da_op; /* not const */
};
/* ---------------------------------------------------------------------- */
/* dynop.c */
struct au_branch;
void au_dy_put(struct au_dykey *key);
int au_dy_iaop(struct inode *inode, aufs_bindex_t bindex,
struct inode *h_inode);
int au_dy_irefresh(struct inode *inode);
void au_dy_arefresh(int do_dio);
void __init au_dy_init(void);
void au_dy_fin(void);
#endif /* __KERNEL__ */
#endif /* __AUFS_DYNOP_H__ */
......@@ -7,7 +7,11 @@
* handling file/dir, and address_space operation
*/
#ifdef CONFIG_AUFS_DEBUG
#include <linux/migrate.h>
#endif
#include <linux/fsnotify.h>
#include <linux/pagemap.h>
#include "aufs.h"
/* drop flags for writing */
......@@ -78,3 +82,91 @@ out_br:
out:
return h_file;
}
/* ---------------------------------------------------------------------- */
/* cf. aufs_nopage() */
/* for madvise(2) */
static int aufs_readpage(struct file *file __maybe_unused, struct page *page)
{
unlock_page(page);
return 0;
}
/* it will never be called, but necessary to support O_DIRECT */
static ssize_t aufs_direct_IO(struct kiocb *iocb, struct iov_iter *iter)
{ BUG(); return 0; }
/* they will never be called. */
#ifdef CONFIG_AUFS_DEBUG
static int aufs_write_begin(struct file *file, struct address_space *mapping,
loff_t pos, unsigned len, unsigned flags,
struct page **pagep, void **fsdata)
{ AuUnsupport(); return 0; }
static int aufs_write_end(struct file *file, struct address_space *mapping,
loff_t pos, unsigned len, unsigned copied,
struct page *page, void *fsdata)
{ AuUnsupport(); return 0; }
static int aufs_writepage(struct page *page, struct writeback_control *wbc)
{ AuUnsupport(); return 0; }
static int aufs_set_page_dirty(struct page *page)
{ AuUnsupport(); return 0; }
static void aufs_invalidatepage(struct page *page, unsigned int offset,
unsigned int length)
{ AuUnsupport(); }
static int aufs_releasepage(struct page *page, gfp_t gfp)
{ AuUnsupport(); return 0; }
#if 0 /* called by memory compaction regardless file */
static int aufs_migratepage(struct address_space *mapping, struct page *newpage,
struct page *page, enum migrate_mode mode)
{ AuUnsupport(); return 0; }
#endif
static bool aufs_isolate_page(struct page *page, isolate_mode_t mode)
{ AuUnsupport(); return true; }
static void aufs_putback_page(struct page *page)
{ AuUnsupport(); }
static int aufs_launder_page(struct page *page)
{ AuUnsupport(); return 0; }
static int aufs_is_partially_uptodate(struct page *page,
unsigned long from,
unsigned long count)
{ AuUnsupport(); return 0; }
static void aufs_is_dirty_writeback(struct page *page, bool *dirty,
bool *writeback)
{ AuUnsupport(); }
static int aufs_error_remove_page(struct address_space *mapping,
struct page *page)
{ AuUnsupport(); return 0; }
static int aufs_swap_activate(struct swap_info_struct *sis, struct file *file,
sector_t *span)
{ AuUnsupport(); return 0; }
static void aufs_swap_deactivate(struct file *file)
{ AuUnsupport(); }
#endif /* CONFIG_AUFS_DEBUG */
const struct address_space_operations aufs_aop = {
.readpage = aufs_readpage,
.direct_IO = aufs_direct_IO,
#ifdef CONFIG_AUFS_DEBUG
.writepage = aufs_writepage,
/* no writepages, because of writepage */
.set_page_dirty = aufs_set_page_dirty,
/* no readpages, because of readpage */
.write_begin = aufs_write_begin,
.write_end = aufs_write_end,
/* no bmap, no block device */
.invalidatepage = aufs_invalidatepage,
.releasepage = aufs_releasepage,
/* is fallback_migrate_page ok? */
/* .migratepage = aufs_migratepage, */
.isolate_page = aufs_isolate_page,
.putback_page = aufs_putback_page,
.launder_page = aufs_launder_page,
.is_partially_uptodate = aufs_is_partially_uptodate,
.is_dirty_writeback = aufs_is_dirty_writeback,
.error_remove_page = aufs_error_remove_page,
.swap_activate = aufs_swap_activate,
.swap_deactivate = aufs_swap_deactivate
#endif /* CONFIG_AUFS_DEBUG */
};
......@@ -35,6 +35,7 @@ struct au_finfo {
/* ---------------------------------------------------------------------- */
/* file.c */
extern const struct address_space_operations aufs_aop;
unsigned int au_file_roflags(unsigned int flags);
struct file *au_h_open(struct dentry *dentry, aufs_bindex_t bindex, int flags,
struct file *file);
......
......@@ -161,6 +161,7 @@ static int __init aufs_init(void)
au_sbilist_init();
sysaufs_brs_init();
au_dy_init();
err = sysaufs_init();
if (unlikely(err))
goto out;
......@@ -184,6 +185,7 @@ out_procfs:
au_procfs_fin();
out_sysaufs:
sysaufs_fin();
au_dy_fin();
out:
return err;
}
......
......@@ -22,6 +22,7 @@ enum {
Opt_trunc_xino_path, Opt_itrunc_xino,
Opt_trunc_xib, Opt_notrunc_xib,
Opt_plink, Opt_noplink, Opt_list_plink,
Opt_dio, Opt_nodio,
Opt_wbr_copyup, Opt_wbr_create,
Opt_tail, Opt_ignore, Opt_ignore_silent, Opt_err
};
......@@ -53,6 +54,9 @@ static match_table_t options = {
{Opt_list_plink, "list_plink"},
#endif
{Opt_dio, "dio"},
{Opt_nodio, "nodio"},
{Opt_wbr_create, "create=%s"},
{Opt_wbr_create, "create_policy=%s"},
{Opt_wbr_copyup, "cpup=%s"},
......@@ -457,6 +461,12 @@ static void dump_opts(struct au_opts *opts)
case Opt_list_plink:
AuLabel(list_plink);
break;
case Opt_dio:
AuLabel(dio);
break;
case Opt_nodio:
AuLabel(nodio);
break;
case Opt_wbr_create:
u.create = &opt->wbr_create;
AuDbg("create %d, %s\n", u.create->wbr_create,
......@@ -714,6 +724,8 @@ int au_opts_parse(struct super_block *sb, char *str, struct au_opts *opts)
case Opt_plink:
case Opt_noplink:
case Opt_list_plink:
case Opt_dio:
case Opt_nodio:
err = 0;
opt->type = token;
break;
......@@ -840,6 +852,15 @@ static int au_opt_simple(struct super_block *sb, struct au_opt *opt,
au_plink_list(sb);
break;
case Opt_dio:
au_opt_set(sbinfo->si_mntflags, DIO);
au_fset_opts(opts->flags, REFRESH_DYAOP);
break;
case Opt_nodio:
au_opt_clr(sbinfo->si_mntflags, DIO);
au_fset_opts(opts->flags, REFRESH_DYAOP);
break;
case Opt_wbr_create:
err = au_opt_wbr_create(sb, &opt->wbr_create);
break;
......
......@@ -25,6 +25,7 @@ struct file;
#define AuOpt_UDBA_NONE (1 << 2) /* users direct branch access */
#define AuOpt_UDBA_REVAL (1 << 3)
#define AuOpt_PLINK (1 << 6) /* pseudo-link */
#define AuOpt_DIO (1 << 14) /* direct io */
#define AuOpt_Def (AuOpt_XINO \
| AuOpt_UDBA_REVAL \
......@@ -120,6 +121,7 @@ struct au_opt {
/* opts flags */
#define AuOpts_TRUNC_XIB (1 << 2)
#define AuOpts_REFRESH_DYAOP (1 << 3)
#define au_ftest_opts(flags, name) ((flags) & AuOpts_##name)
#define au_fset_opts(flags, name) \
do { (flags) |= AuOpts_##name; } while (0)
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment