diff --git a/btrfs.go b/btrfs.go
index 5d8e054..478b43a 100644
--- a/btrfs.go
+++ b/btrfs.go
@@ -1,13 +1,13 @@
 package btrfs
 
 import (
+	"fmt"
 	"github.com/dennwc/btrfs/ioctl"
+	"io"
 	"os"
+	"path/filepath"
 )
 
-//go:generate go run cmd/hgen.go -p btrfs -o btrfs_tree_hc.go btrfs_tree.h
-//go:generate go fmt -w btrfs_tree_hc.go
-
 const SuperMagic = 0x9123683E
 
 func Open(path string) (*FS, error) {
@@ -19,6 +19,12 @@ func Open(path string) (*FS, error) {
 	dir, err := os.Open(path)
 	if err != nil {
 		return nil, err
+	} else if st, err := dir.Stat(); err != nil {
+		dir.Close()
+		return nil, err
+	} else if !st.IsDir() {
+		dir.Close()
+		return nil, fmt.Errorf("not a directory: %s", path)
 	}
 	return &FS{f: dir}, nil
 }
@@ -139,3 +145,39 @@ func (f *FS) Sync() (err error) {
 	}
 	return ioctl.Do(f.f, _BTRFS_IOC_WAIT_SYNC, nil)
 }
+
+func (f *FS) CreateSubVolume(name string) error {
+	return CreateSubVolume(filepath.Join(f.f.Name(), name))
+}
+
+func (f *FS) DeleteSubVolume(name string) error {
+	return DeleteSubVolume(filepath.Join(f.f.Name(), name))
+}
+
+func (f *FS) Snapshot(dst string, ro bool) error {
+	return SnapshotSubVolume(f.f.Name(), filepath.Join(f.f.Name(), dst), ro)
+}
+
+func (f *FS) SnapshotSubVolume(name string, dst string, ro bool) error {
+	return SnapshotSubVolume(filepath.Join(f.f.Name(), name),
+		filepath.Join(f.f.Name(), dst), ro)
+}
+
+func (f *FS) Send(w io.Writer, parent string, subvols ...string) error {
+	if parent != "" {
+		parent = filepath.Join(f.f.Name(), parent)
+	}
+	sub := make([]string, 0, len(subvols))
+	for _, s := range subvols {
+		sub = append(sub, filepath.Join(f.f.Name(), s))
+	}
+	return Send(w, parent, sub...)
+}
+
+func (f *FS) Receive(r io.Reader) error {
+	return Receive(r, f.f.Name())
+}
+
+func (f *FS) ReceiveTo(r io.Reader, mount string) error {
+	return Receive(r, filepath.Join(f.f.Name(), mount))
+}
diff --git a/btrfs_list.go b/btrfs_list.go
new file mode 100644
index 0000000..4b4a941
--- /dev/null
+++ b/btrfs_list.go
@@ -0,0 +1,13 @@
+package btrfs
+
+import "os"
+
+func getPathRootID(file *os.File) (uint64, error) {
+	args := btrfs_ioctl_ino_lookup_args{
+		objectid: firstFreeObjectid,
+	}
+	if err := iocInoLookup(file, &args); err != nil {
+		return 0, err
+	}
+	return args.treeid, nil
+}
diff --git a/btrfs_tree_h.go b/btrfs_tree_h.go
deleted file mode 100644
index e35273d..0000000
--- a/btrfs_tree_h.go
+++ /dev/null
@@ -1,23 +0,0 @@
-package btrfs
-
-// This header contains the structure definitions and constants used
-// by file system objects that can be retrieved using
-// the BTRFS_IOC_SEARCH_TREE ioctl.  That means basically anything that
-// is needed to describe a leaf node's key or item contents.
-
-const (
-	// Holds pointers to all of the tree roots
-	BTRFS_ROOT_TREE_OBJECTID = 1
-
-	// Stores information about which extents are in use, and reference counts
-	BTRFS_EXTENT_TREE_OBJECTID = 2
-
-	// Chunk tree stores translations from logical -> physical block numbering
-	// the super block points to the chunk tree.
-	BTRFS_CHUNK_TREE_OBJECTID = 3
-
-	// All files have objectids in this range.
-	BTRFS_FIRST_FREE_OBJECTID       = 256
-	BTRFS_LAST_FREE_OBJECTID        = 0xffffff00 // -256
-	BTRFS_FIRST_CHUNK_TREE_OBJECTID = 256
-)
diff --git a/btrfs_tree_hc.go b/btrfs_tree_hc.go
new file mode 100644
index 0000000..5b74214
--- /dev/null
+++ b/btrfs_tree_hc.go
@@ -0,0 +1,482 @@
+package btrfs
+
+// This code was auto-generated; DO NOT EDIT!
+
+// This header contains the structure definitions and constants used
+// by file system objects that can be retrieved using
+// the BTRFS_IOC_SEARCH_TREE ioctl. That means basically anything that
+// is needed to describe a leaf node's key or item contents.
+
+const (
+	// Holds pointers to all of the tree roots
+	rootTreeObjectid = 1
+
+	// Stores information about which extents are in use, and reference counts
+	extentTreeObjectid = 2
+
+	// Chunk tree stores translations from logical -> physical block numbering
+	// the super block points to the chunk tree
+	chunkTreeObjectid = 3
+
+	// Stores information about which areas of a given device are in use.
+	// one per device. The tree of tree roots points to the device tree
+	devTreeObjectid = 4
+
+	// One per subvolume, storing files and directories
+	fsTreeObjectid = 5
+
+	// Directory objectid inside the root tree
+	rootTreeDirObjectid = 6
+
+	// Holds checksums of all the data extents
+	csumTreeObjectid = 7
+
+	// Holds quota configuration and tracking
+	quotaTreeObjectid = 8
+
+	// For storing items that use the BTRFS_UUID_KEY* types
+	uuidTreeObjectid = 9
+
+	// Tracks free space in block groups.
+	freeSpaceTreeObjectid = 10
+
+	// Device stats in the device tree
+	devStatsObjectid = 0
+
+	// For storing balance parameters in the root tree
+	balanceObjectid = 0xfffffffffffffffc /* -4 */
+
+	// Orhpan objectid for tracking unlinked/truncated files
+	orphanObjectid = 0xfffffffffffffffb /* -5 */
+
+	// Does write ahead logging to speed up fsyncs
+	treeLogObjectid      = 0xfffffffffffffffa /* -6 */
+	treeLogFixupObjectid = 0xfffffffffffffff9 /* -7 */
+
+	// For space balancing
+	treeRelocObjectid     = 0xfffffffffffffff8 /* -8 */
+	dataRelocTreeObjectid = 0xfffffffffffffff7 /* -9 */
+
+	// Extent checksums all have this objectid
+	// this allows them to share the logging tree
+	// for fsyncs
+	extentCsumObjectid = 0xfffffffffffffff6 /* -10 */
+
+	// For storing free space cache
+	freeSpaceObjectid = 0xfffffffffffffff5 /* -11 */
+
+	// The inode number assigned to the special inode for storing
+	// free ino cache
+	freeInoObjectid = 0xfffffffffffffff4 /* -12 */
+
+	// Dummy objectid represents multiple objectids
+	multipleObjectids = 0xffffffffffffff01 /* -255 */
+
+	// All files have objectids in this range.
+	firstFreeObjectid      = 256
+	lastFreeObjectid       = 0xffffffffffffff00 /* -256 */
+	firstChunkTreeObjectid = 256
+
+	// The device items go into the chunk tree. The key is in the form
+	// [ 1 BTRFS_DEV_ITEM_KEY device_id ]
+	devItemsObjectid = 1
+
+	btreeInodeObjectid = 1
+
+	emptySubvolDirObjectid = 2
+
+	devReplaceDevid = 0
+
+	// Inode items have the data typically returned from stat and store other
+	// info about object characteristics. There is one for every file and dir in
+	// the FS
+	inodeItemKey   = 1
+	inodeRefKey    = 12
+	inodeExtrefKey = 13
+	xattrItemKey   = 24
+	orphanItemKey  = 48
+	// Reserve 2-15 close to the inode for later flexibility
+
+	// Dir items are the name -> inode pointers in a directory. There is one
+	// for every name in a directory.
+	dirLogItemKey  = 60
+	dirLogIndexKey = 72
+	dirItemKey     = 84
+	dirIndexKey    = 96
+	// Extent data is for file data
+	extentDataKey = 108
+
+	// Extent csums are stored in a separate tree and hold csums for
+	// an entire extent on disk.
+	extentCsumKey = 128
+
+	// Root items point to tree roots. They are typically in the root
+	// tree used by the super block to find all the other trees
+	rootItemKey = 132
+
+	// Root backrefs tie subvols and snapshots to the directory entries that
+	// reference them
+	rootBackrefKey = 144
+
+	// Root refs make a fast index for listing all of the snapshots and
+	// subvolumes referenced by a given root. They point directly to the
+	// directory item in the root that references the subvol
+	rootRefKey = 156
+
+	// Extent items are in the extent map tree. These record which blocks
+	// are used, and how many references there are to each block
+	extentItemKey = 168
+
+	// The same as the BTRFS_EXTENT_ITEM_KEY, except it's metadata we already know
+	// the length, so we save the level in key->offset instead of the length.
+	metadataItemKey = 169
+
+	treeBlockRefKey = 176
+
+	extentDataRefKey = 178
+
+	sharedBlockRefKey = 182
+
+	sharedDataRefKey = 184
+
+	// Block groups give us hints into the extent allocation trees. Which
+	// blocks are free etc etc
+	blockGroupItemKey = 192
+
+	// Every block group is represented in the free space tree by a free space info
+	// item, which stores some accounting information. It is keyed on
+	// (block_group_start, FREE_SPACE_INFO, block_group_length).
+	freeSpaceInfoKey = 198
+
+	// A free space extent tracks an extent of space that is free in a block group.
+	// It is keyed on (start, FREE_SPACE_EXTENT, length).
+	freeSpaceExtentKey = 199
+
+	// When a block group becomes very fragmented, we convert it to use bitmaps
+	// instead of extents. A free space bitmap is keyed on
+	// (start, FREE_SPACE_BITMAP, length); the corresponding item is a bitmap with
+	// (length / sectorsize) bits.
+	freeSpaceBitmapKey = 200
+
+	devExtentKey = 204
+	devItemKey   = 216
+	chunkItemKey = 228
+
+	// Records the overall state of the qgroups.
+	// There's only one instance of this key present,
+	// (0, BTRFS_QGROUP_STATUS_KEY, 0)
+	qgroupStatusKey = 240
+	// Records the currently used space of the qgroup.
+	// One key per qgroup, (0, BTRFS_QGROUP_INFO_KEY, qgroupid).
+	qgroupInfoKey = 242
+	// Contains the user configured limits for the qgroup.
+	// One key per qgroup, (0, BTRFS_QGROUP_LIMIT_KEY, qgroupid).
+	qgroupLimitKey = 244
+	// Records the child-parent relationship of qgroups. For
+	// each relation, 2 keys are present:
+	// (childid, BTRFS_QGROUP_RELATION_KEY, parentid)
+	// (parentid, BTRFS_QGROUP_RELATION_KEY, childid)
+	qgroupRelationKey = 246
+
+	// Obsolete name, see BTRFS_TEMPORARY_ITEM_KEY.
+	balanceItemKey = 248
+
+	// The key type for tree items that are stored persistently, but do not need to
+	// exist for extended period of time. The items can exist in any tree.
+	// [subtype, BTRFS_TEMPORARY_ITEM_KEY, data]
+	// Existing items:
+	// - balance status item
+	// (BTRFS_BALANCE_OBJECTID, BTRFS_TEMPORARY_ITEM_KEY, 0)
+	temporaryItemKey = 248
+
+	// Obsolete name, see BTRFS_PERSISTENT_ITEM_KEY
+	devStatsKey = 249
+
+	// The key type for tree items that are stored persistently and usually exist
+	// for a long period, eg. filesystem lifetime. The item kinds can be status
+	// information, stats or preference values. The item can exist in any tree.
+	// [subtype, BTRFS_PERSISTENT_ITEM_KEY, data]
+	// Existing items:
+	// - device statistics, store IO stats in the device tree, one key for all
+	// stats
+	// (BTRFS_DEV_STATS_OBJECTID, BTRFS_DEV_STATS_KEY, 0)
+	persistentItemKey = 249
+
+	// Persistantly stores the device replace state in the device tree.
+	// The key is built like this: (0, BTRFS_DEV_REPLACE_KEY, 0).
+	devReplaceKey = 250
+
+	// Stores items that allow to quickly map UUIDs to something else.
+	// These items are part of the filesystem UUID tree.
+	// The key is built like this:
+	// (UUID_upper_64_bits, BTRFS_UUID_KEY*, UUID_lower_64_bits).
+	uuidKeySubvol         = 251
+	uuidKeyReceivedSubvol = 252
+
+	// String items are for debugging. They just store a short string of
+	// data in the FS
+	stringItemKey = 253
+
+	// 32 bytes in various csum fields
+	csumSize = 32
+
+	// Csum types
+
+	// Flags definitions for directory entry item type
+	// Used by:
+	// struct btrfs_dir_item.type
+	ftUnknown = 0
+	ftRegFile = 1
+	ftDir     = 2
+	ftChrdev  = 3
+	ftBlkdev  = 4
+	ftFifo    = 5
+	ftSock    = 6
+	ftSymlink = 7
+	ftXattr   = 8
+	ftMax     = 9
+
+	// The key defines the order in the tree, and so it also defines (optimal)
+	// block layout.
+	// objectid corresponds to the inode number.
+	// type tells us things about the object, and is a kind of stream selector.
+	// so for a given inode, keys with type of 1 might refer to the inode data,
+	// type of 2 may point to file data in the btree and type == 3 may point to
+	// extents.
+	// offset is the starting byte offset for this key in the stream.
+	// btrfs_disk_key is in disk byte order. struct btrfs_key is always
+	// in cpu native order. Otherwise they are identical and their sizes
+	// should be the same (ie both packed)
+
+	// The internal btrfs device id
+
+	// Size of the device
+
+	// Bytes used
+
+	// Optimal io alignment for this device
+
+	// Optimal io width for this device
+
+	// Minimal io size for this device
+
+	// Type and info about this device
+
+	// Expected generation for this device
+
+	// Starting byte of this partition on the device,
+	// to allow for stripe alignment in the future
+
+	// Grouping information for allocation decisions
+
+	// Seek speed 0-100 where 100 is fastest
+
+	// Bandwidth 0-100 where 100 is fastest
+
+	// Btrfs generated uuid for this device
+
+	// Uuid of FS who owns this device
+
+	// Size of this chunk in bytes
+
+	// Objectid of the root referencing this chunk
+
+	// Optimal io alignment for this chunk
+
+	// Optimal io width for this chunk
+
+	// Minimal io size for this chunk
+
+	// 2^16 stripes is quite a lot, a second limit is the size of a single
+	// item in the btree
+
+	// Sub stripes only matter for raid10
+	// Additional stripes go here
+
+	freeSpaceExtent = 1
+	freeSpaceBitmap = 2
+
+	headerFlagWritten = (1 << 0)
+	headerFlagReloc   = (1 << 1)
+
+	// Super block flags
+	// Errors detected
+	superFlagError = (1 << 2)
+
+	superFlagSeeding  = (1 << 32)
+	superFlagMetadump = (1 << 33)
+
+	// Items in the extent btree are used to record the objectid of the
+	// owner of the block and the number of references
+
+	extentFlagData      = (1 << 0)
+	extentFlagTreeBlock = (1 << 1)
+
+	// Following flags only apply to tree blocks
+
+	// Use full backrefs for extent pointers in the block
+	blockFlagFullBackref = (1 << 8)
+
+	// This flag is only used internally by scrub and may be changed at any time
+	// it is only declared here to avoid collisions
+	extentFlagSuper = (1 << 48)
+
+	// Old style backrefs item
+
+	// Dev extents record free space on individual devices. The owner
+	// field points back to the chunk allocation mapping tree that allocated
+	// the extent. The chunk tree uuid field is a way to double check the owner
+
+	// Name goes here
+
+	// Name goes here
+
+	// Nfs style generation number
+	// Transid that last touched this inode
+
+	// Modification sequence number for NFS
+
+	// A little future expansion, for more than this we can
+	// just grow the inode item and version it
+
+	rootSubvolRdonly = (1 << 0)
+
+	// Internal in-memory flag that a subvolume has been marked for deletion but
+	// still visible as a directory
+	rootSubvolDead = (1 << 48)
+
+	// The following fields appear after subvol_uuids+subvol_times
+	// were introduced.
+
+	// This generation number is used to test if the new fields are valid
+	// and up to date while reading the root item. Every time the root item
+	// is written out, the "generation" field is copied into this field. If
+	// anyone ever mounted the fs with an older kernel, we will have
+	// mismatching generation values here and thus must invalidate the
+	// new fields. See btrfs_update_root and btrfs_find_last_root for
+	// details.
+	// the offset of generation_v2 is also used as the start for the memset
+	// when invalidating the fields.
+
+	// This is used for both forward and backward root refs
+
+	// Profiles to operate on, single is denoted by
+	// BTRFS_AVAIL_ALLOC_BIT_SINGLE
+
+	// Usage filter
+	// BTRFS_BALANCE_ARGS_USAGE with a single value means '0..N'
+	// BTRFS_BALANCE_ARGS_USAGE_RANGE - range syntax, min..max
+
+	// Devid filter
+
+	// Devid subset filter [pstart..pend)
+
+	// Btrfs virtual address space subset filter [vstart..vend)
+
+	// Profile to convert to, single is denoted by
+	// BTRFS_AVAIL_ALLOC_BIT_SINGLE
+
+	// BTRFS_BALANCE_ARGS_*
+
+	// BTRFS_BALANCE_ARGS_LIMIT with value 'limit'
+	// BTRFS_BALANCE_ARGS_LIMIT_RANGE - the extend version can use minimum
+	// and maximum
+
+	// Process chunks that cross stripes_min..stripes_max devices,
+	// BTRFS_BALANCE_ARGS_STRIPES_RANGE
+
+	// Store balance parameters to disk so that balance can be properly
+	// resumed after crash or unmount
+	// BTRFS_BALANCE_*
+
+	fileExtentInline   = 0
+	fileExtentReg      = 1
+	fileExtentPrealloc = 2
+
+	// Transaction id that created this extent
+	// Max number of bytes to hold this extent in ram
+	// when we split a compressed extent we can't know how big
+	// each of the resulting pieces will be. So, this is
+	// an upper limit on the size of the extent in ram instead of
+	// an exact limit.
+
+	// 32 bits for the various ways we might encode the data,
+	// including compression and encryption. If any of these
+	// are set to something a given disk format doesn't understand
+	// it is treated like an incompat flag for reading and writing,
+	// but not for stat.
+
+	// Are we inline data or a real extent?
+
+	// Disk space consumed by the extent, checksum blocks are included
+	// in these numbers
+	// At this offset in the structure, the inline extent data start.
+	// The logical offset in file blocks (no csums)
+	// this extent record is for. This allows a file extent to point
+	// into the middle of an existing extent on disk, sharing it
+	// between two snapshots (useful if some bytes in the middle of the
+	// extent have changed
+	// The logical number of file blocks (no csums included). This
+	// always reflects the size uncompressed and without encoding.
+
+	// Grow this item struct at the end for future enhancements and keep
+	// the existing values unchanged
+
+	devReplaceItemContReadingFromSrcdevModeAlways = 0
+	devReplaceItemContReadingFromSrcdevModeAvoid  = 1
+	devReplaceItemStateNeverStarted               = 0
+	devReplaceItemStateStarted                    = 1
+	devReplaceItemStateSuspended                  = 2
+	devReplaceItemStateFinished                   = 3
+	devReplaceItemStateCanceled                   = 4
+
+	// Grow this item struct at the end for future enhancements and keep
+	// the existing values unchanged
+
+	// Different types of block groups (and chunks)
+	blockGroupData     = (1 << 0)
+	blockGroupSystem   = (1 << 1)
+	blockGroupMetadata = (1 << 2)
+	blockGroupDup      = (1 << 5)
+
+	// We need a bit for restriper to be able to tell when chunks of type
+	// SINGLE are available. This "extended" profile format is used in
+	// fs_info->avail_*_alloc_bits (in-memory) and balance item fields
+	// (on-disk). The corresponding on-disk bit in chunk.type is reserved
+	// to avoid remappings between two formats in future.
+	availAllocBitSingle = (1 << 48)
+
+	// A fake block group type that is used to communicate global block reserve
+	// size to userspace via the SPACE_INFO ioctl.
+	spaceInfoGlobalRsv = (1 << 49)
+
+	freeSpaceUsingBitmaps = (1 << 0)
+
+	qgroupLevelShift = 48
+
+	// Is subvolume quota turned on?
+	qgroupStatusFlagOn = (1 << 0)
+	// RESCAN is set during the initialization phase
+	qgroupStatusFlagRescan = (1 << 1)
+	// Some qgroup entries are known to be out of date,
+	// either because the configuration has changed in a way that
+	// makes a rescan necessary, or because the fs has been mounted
+	// with a non-qgroup-aware version.
+	// Turning qouta off and on again makes it inconsistent, too.
+	qgroupStatusFlagInconsistent = (1 << 2)
+
+	qgroupStatusVersion = 1
+
+	// The generation is updated during every commit. As older
+	// versions of btrfs are not aware of qgroups, it will be
+	// possible to detect inconsistencies by checking the
+	// generation on mount time
+
+	// Flag definitions see above
+
+	// Only used during scanning to record the progress
+	// of the scan. It contains a logical address
+
+	// Only updated when any of the other values change
+
+)
diff --git a/cmd/gbtrfs.go b/cmd/gbtrfs.go
new file mode 100644
index 0000000..2389992
--- /dev/null
+++ b/cmd/gbtrfs.go
@@ -0,0 +1,94 @@
+package main
+
+import (
+	"fmt"
+	"github.com/dennwc/btrfs"
+	"github.com/spf13/cobra"
+	"os"
+)
+
+func init() {
+	RootCmd.AddCommand(
+		SubvolumeCmd,
+		SendCmd,
+		ReceiveCmd,
+	)
+
+	SubvolumeCmd.AddCommand(
+		SubvolumeCreateCmd,
+		SubvolumeDeleteCmd,
+	)
+}
+
+var RootCmd = &cobra.Command{
+	Use:   "btrfs [--help] [--version] <group> [<group>...] <command> [<args>]",
+	Short: "Use --help as an argument for information on a specific group or command.",
+}
+
+var SubvolumeCmd = &cobra.Command{
+	Use: "subvolume <command> <args>",
+}
+
+var SubvolumeCreateCmd = &cobra.Command{
+	Use:   "create [-i <qgroupid>] [<dest>/]<name>",
+	Short: "Create a subvolume",
+	Long:  `Create a subvolume <name> in <dest>.  If <dest> is not given subvolume <name> will be created in the current directory.`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) == 0 {
+			return fmt.Errorf("subvolume not specified")
+		} else if len(args) > 1 {
+			return fmt.Errorf("only one subvolume name is allowed")
+		}
+		return btrfs.CreateSubVolume(args[0])
+	},
+}
+
+var SubvolumeDeleteCmd = &cobra.Command{
+	Use:   "delete [options] <subvolume> [<subvolume>...]",
+	Short: "Delete subvolume(s)",
+	Long: `Delete subvolumes from the filesystem. The corresponding directory
+is removed instantly but the data blocks are removed later.
+The deletion does not involve full commit by default due to
+performance reasons (as a consequence, the subvolume may appear again
+after a crash). Use one of the --commit options to wait until the
+operation is safely stored on the media.`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		for _, arg := range args {
+			if err := btrfs.DeleteSubVolume(arg); err != nil {
+				return err
+			}
+		}
+		return nil
+	},
+}
+
+var SendCmd = &cobra.Command{
+	Use:   "send [-ve] [-p <parent>] [-c <clone-src>] [-f <outfile>] <subvol> [<subvol>...]",
+	Short: "Send the subvolume(s) to stdout.",
+	Long: `Sends the subvolume(s) specified by <subvol> to stdout.
+<subvol> should be read-only here.`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		return btrfs.Send(os.Stdout, "", args...)
+	},
+}
+
+var ReceiveCmd = &cobra.Command{
+	Use:   "receive [-ve] [-f <infile>] [--max-errors <N>] <mount>",
+	Short: "Receive subvolumes from stdin.",
+	Long: `Receives one or more subvolumes that were previously
+sent with btrfs send. The received subvolumes are stored
+into <mount>.`,
+	RunE: func(cmd *cobra.Command, args []string) error {
+		if len(args) != 1 {
+			return fmt.Errorf("expected one destination argument")
+		}
+		return btrfs.Receive(os.Stdin, args[0])
+	},
+}
+
+func main() {
+	if err := RootCmd.Execute(); err != nil {
+		fmt.Println(err)
+		os.Exit(-1)
+	}
+}
diff --git a/cmd/hgen.go b/cmd/hgen.go
index 33bbc9a..02616a4 100644
--- a/cmd/hgen.go
+++ b/cmd/hgen.go
@@ -9,20 +9,47 @@ import (
 	"log"
 	"os"
 	"regexp"
+	"strconv"
 	"strings"
 	"unicode"
 )
 
 var (
-	f_pkg = flag.String("p", "main", "package name for generated file")
-	f_out = flag.String("o", "-", "output file")
+	f_pkg      = flag.String("p", "main", "package name for generated file")
+	f_out      = flag.String("o", "-", "output file")
+	f_unexport = flag.Bool("u", true, "make all definitions unexported")
+	f_goname   = flag.Bool("g", true, "rename symbols to follow Go conventions")
+	f_trim     = flag.String("t", "", "prefix to trim from names")
 )
 
 var (
-	reDefineIntConst = regexp.MustCompile(`#define\s+([A-Za-z_]+)\s+(\(?\d+(?:U?LL)?(?:\s*<<\s*\d+)?\)?)`)
+	reDefineIntConst = regexp.MustCompile(`#define\s+([A-Za-z_]+)\s+(\(?-?\d+(?:U?LL)?(?:\s*<<\s*\d+)?\)?)`)
+	reNegULL         = regexp.MustCompile(`-(\d+)ULL`)
 )
 
 func constName(s string) string {
+	s = strings.TrimPrefix(s, *f_trim)
+	if *f_goname {
+		buf := bytes.NewBuffer(nil)
+		buf.Grow(len(s))
+		up := !*f_unexport
+		for _, r := range s {
+			if r == '_' {
+				up = true
+				continue
+			}
+			if up {
+				up = false
+				r = unicode.ToUpper(r)
+			} else {
+				r = unicode.ToLower(r)
+			}
+			buf.WriteRune(r)
+		}
+		s = buf.String()
+	} else if *f_unexport {
+		s = "_" + s
+	}
 	return s
 }
 
@@ -41,6 +68,7 @@ func process(w io.Writer, path string) error {
 	)
 
 	nl := true
+	fmt.Fprint(w, "// This code was auto-generated; DO NOT EDIT!\n\n")
 	defer fmt.Fprintln(w, ")")
 	for {
 		line, err := r.ReadBytes('\n')
@@ -96,6 +124,15 @@ func process(w io.Writer, path string) error {
 			sub := reDefineIntConst.FindStringSubmatch(string(line))
 			if len(sub) > 0 {
 				name, val := sub[1], sub[2]
+				if sub := reNegULL.FindAllStringSubmatch(val, -1); len(sub) > 0 {
+					for _, s := range sub {
+						v, err := strconv.ParseInt(s[1], 10, 64)
+						if err != nil {
+							panic(err)
+						}
+						val = strings.Replace(val, s[0], fmt.Sprintf("0x%x /* -%s */", uint64(-v), s[1]), -1)
+					}
+				}
 				val = strings.Replace(val, "ULL", "", -1)
 				fmt.Fprintf(w, "\t%s = %s\n", constName(name), val)
 				continue
diff --git a/errors.go b/errors.go
index 5259ae7..cda99f8 100644
--- a/errors.go
+++ b/errors.go
@@ -7,7 +7,7 @@ type ErrNotBtrfs struct {
 }
 
 func (e ErrNotBtrfs) Error() string {
-	return fmt.Sprintf("path %q is not a btrfs", e.Path)
+	return fmt.Sprintf("not a btrfs filesystem: %s", e.Path)
 }
 
 // Error codes as returned by the kernel
diff --git a/headers.go b/headers.go
new file mode 100644
index 0000000..f0b0a0f
--- /dev/null
+++ b/headers.go
@@ -0,0 +1,4 @@
+package btrfs
+
+//go:generate go run ./cmd/hgen.go -u -g -t BTRFS_ -p btrfs -o btrfs_tree_hc.go btrfs_tree.h
+//go:generate gofmt -l -w btrfs_tree_hc.go
diff --git a/ioctl/ioctl.go b/ioctl/ioctl.go
index d2061a0..0571c5b 100644
--- a/ioctl/ioctl.go
+++ b/ioctl/ioctl.go
@@ -65,10 +65,6 @@ func Ioctl(f *os.File, ioc uintptr, addr uintptr) error {
 	return nil
 }
 
-func SizeOf(arg interface{}) uintptr {
-	return reflect.TypeOf(arg).Size()
-}
-
 func Do(f *os.File, ioc uintptr, arg interface{}) error {
 	var addr uintptr
 	if arg != nil {
diff --git a/ioctl_h.go b/ioctl_h.go
index 7613fa9..386246d 100644
--- a/ioctl_h.go
+++ b/ioctl_h.go
@@ -64,10 +64,26 @@ type btrfs_ioctl_vol_args_v2_u1 struct {
 
 const subvolNameMax = 4039
 
+type subvolFlags uint64
+
+// flags for subvolumes
+//
+// Used by:
+// struct btrfs_ioctl_vol_args_v2.flags
+//
+// BTRFS_SUBVOL_RDONLY is also provided/consumed by the following ioctls:
+// - BTRFS_IOC_SUBVOL_GETFLAGS
+// - BTRFS_IOC_SUBVOL_SETFLAGS
+const (
+	subvolCreateAsync   = subvolFlags(1 << 0)
+	subvolReadOnly      = subvolFlags(1 << 1)
+	subvolQGroupInherit = subvolFlags(1 << 2)
+)
+
 type btrfs_ioctl_vol_args_v2 struct {
 	fd      int64
 	transid uint64
-	flags   uint64
+	flags   subvolFlags
 	btrfs_ioctl_vol_args_v2_u1
 	unused [2]uint64
 	name   [subvolNameMax + 1]byte
@@ -267,7 +283,7 @@ type btrfs_ioctl_search_key struct {
 	max_type uint32
 	// how many items did userland ask for, and how many are we returning
 	nr_items uint32
-	unused   [4 + 4*8]byte
+	unused   [36]byte
 }
 
 type btrfs_ioctl_search_header struct {
@@ -507,6 +523,8 @@ var (
 	_BTRFS_IOC_SPACE_INFO             = ioctl.IOWR(ioctlMagic, 20, unsafe.Sizeof(btrfs_ioctl_space_args{}))
 	_BTRFS_IOC_START_SYNC             = ioctl.IOR(ioctlMagic, 24, 8) // uint64
 	_BTRFS_IOC_WAIT_SYNC              = ioctl.IOW(ioctlMagic, 22, 8) // uint64
+	_BTRFS_IOC_SNAP_CREATE_V2         = ioctl.IOW(ioctlMagic, 23, unsafe.Sizeof(btrfs_ioctl_vol_args_v2{}))
+	_BTRFS_IOC_SUBVOL_CREATE_V2       = ioctl.IOW(ioctlMagic, 24, unsafe.Sizeof(btrfs_ioctl_vol_args_v2{}))
 	_BTRFS_IOC_SUBVOL_GETFLAGS        = ioctl.IOR(ioctlMagic, 25, 8) // uint64
 	_BTRFS_IOC_SUBVOL_SETFLAGS        = ioctl.IOW(ioctlMagic, 26, 8) // uint64
 	_BTRFS_IOC_SCRUB                  = ioctl.IOWR(ioctlMagic, 27, unsafe.Sizeof(btrfs_ioctl_scrub_args{}))
@@ -538,8 +556,12 @@ var (
 	_BTRFS_IOC_GET_SUPPORTED_FEATURES = ioctl.IOR(ioctlMagic, 57, unsafe.Sizeof([3]btrfs_ioctl_feature_flags{}))
 )
 
-func iocSnapCreate(f *os.File, out *btrfs_ioctl_vol_args) error {
-	return ioctl.Do(f, _BTRFS_IOC_SNAP_CREATE, out)
+func iocSnapCreate(f *os.File, in *btrfs_ioctl_vol_args) error {
+	return ioctl.Do(f, _BTRFS_IOC_SNAP_CREATE, in)
+}
+
+func iocSnapCreateV2(f *os.File, in *btrfs_ioctl_vol_args_v2) error {
+	return ioctl.Do(f, _BTRFS_IOC_SNAP_CREATE_V2, in)
 }
 
 func iocDefrag(f *os.File, out *btrfs_ioctl_vol_args) error {
@@ -586,12 +608,16 @@ func iocCloneRange(f *os.File, out *btrfs_ioctl_clone_range_args) error {
 	return ioctl.Do(f, _BTRFS_IOC_CLONE_RANGE, out)
 }
 
-func iocSubvolCreate(f *os.File, out *btrfs_ioctl_vol_args) error {
-	return ioctl.Do(f, _BTRFS_IOC_SUBVOL_CREATE, out)
+func iocSubvolCreate(f *os.File, in *btrfs_ioctl_vol_args) error {
+	return ioctl.Do(f, _BTRFS_IOC_SUBVOL_CREATE, in)
 }
 
-func iocSnapDestroy(f *os.File, out *btrfs_ioctl_vol_args) error {
-	return ioctl.Do(f, _BTRFS_IOC_SNAP_DESTROY, out)
+func iocSubvolCreateV2(f *os.File, in *btrfs_ioctl_vol_args_v2) error {
+	return ioctl.Do(f, _BTRFS_IOC_SUBVOL_CREATE, in)
+}
+
+func iocSnapDestroy(f *os.File, in *btrfs_ioctl_vol_args) error {
+	return ioctl.Do(f, _BTRFS_IOC_SNAP_DESTROY, in)
 }
 
 func iocDefragRange(f *os.File, out *btrfs_ioctl_defrag_range_args) error {
diff --git a/receive.go b/receive.go
new file mode 100644
index 0000000..3e2a74a
--- /dev/null
+++ b/receive.go
@@ -0,0 +1,32 @@
+package btrfs
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"os/exec"
+)
+
+func Receive(r io.Reader, mount string) error {
+	// TODO: write a native implementation?
+	//tf, err := ioutil.TempFile("","btrfs_snap")
+	//if err != nil {
+	//	return err
+	//}
+	//defer func(){
+	//	name := tf.Name()
+	//	tf.Close()
+	//	os.Remove(name)
+	//}()
+	buf := bytes.NewBuffer(nil)
+	cmd := exec.Command("btrfs", "receive", mount)
+	cmd.Stdin = r
+	cmd.Stderr = buf
+	if err := cmd.Run(); err != nil {
+		if buf.Len() != 0 {
+			return errors.New(buf.String())
+		}
+		return err
+	}
+	return nil
+}
diff --git a/send.go b/send.go
new file mode 100644
index 0000000..4c4b584
--- /dev/null
+++ b/send.go
@@ -0,0 +1,46 @@
+package btrfs
+
+import (
+	"bytes"
+	"errors"
+	"io"
+	"io/ioutil"
+	"os"
+	"os/exec"
+)
+
+func Send(w io.Writer, parent string, subvols ...string) error {
+	if len(subvols) == 0 {
+		return nil
+	}
+	// TODO: write a native implementation?
+	args := []string{
+		"send",
+	}
+	if parent != "" {
+		args = append(args, "-p", parent)
+	}
+	tf, err := ioutil.TempFile("", "btrfs_snap")
+	if err != nil {
+		return err
+	}
+	defer func() {
+		name := tf.Name()
+		tf.Close()
+		os.Remove(name)
+	}()
+	args = append(args, "-f", tf.Name())
+	buf := bytes.NewBuffer(nil)
+	args = append(args, subvols...)
+	cmd := exec.Command("btrfs", args...)
+	cmd.Stderr = buf
+	if err = cmd.Run(); err != nil {
+		if buf.Len() != 0 {
+			return errors.New(buf.String())
+		}
+		return err
+	}
+	tf.Seek(0, 0)
+	_, err = io.Copy(w, tf)
+	return err
+}
diff --git a/subvolume.go b/subvolume.go
index be7db56..42ee9c2 100644
--- a/subvolume.go
+++ b/subvolume.go
@@ -1,21 +1,141 @@
 package btrfs
 
 import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
 	"syscall"
 )
 
+func checkSubVolumeName(name string) bool {
+	return name != "" && name[0] != 0 && !strings.ContainsRune(name, '/') &&
+		name != "." && name != ".."
+}
+
 func IsSubVolume(path string) (bool, error) {
 	var st syscall.Stat_t
 	if err := syscall.Stat(path, &st); err != nil {
 		return false, err
 	}
-	if st.Ino != BTRFS_FIRST_FREE_OBJECTID ||
+	if st.Ino != firstFreeObjectid ||
 		st.Mode&syscall.S_IFMT != syscall.S_IFDIR {
 		return false, nil
 	}
-	var stfs syscall.Statfs_t
-	if err := syscall.Statfs(path, &stfs); err != nil {
-		return false, err
-	}
-	return stfs.Type == SuperMagic, nil
+	return isBtrfs(path)
+}
+
+func CreateSubVolume(path string) error {
+	var inherit *btrfs_qgroup_inherit // TODO
+
+	cpath, err := filepath.Abs(path)
+	if err != nil {
+		return err
+	}
+	newName := filepath.Base(cpath)
+	dstDir := filepath.Dir(cpath)
+	if !checkSubVolumeName(newName) {
+		return fmt.Errorf("invalid subvolume name: %s", newName)
+	} else if len(newName) >= volNameMax {
+		return fmt.Errorf("subvolume name too long: %s", newName)
+	}
+	dst, err := openDir(dstDir)
+	if err != nil {
+		return err
+	}
+	defer dst.Close()
+	if inherit != nil {
+		panic("not implemented") // TODO
+		args := btrfs_ioctl_vol_args_v2{
+			flags: subvolQGroupInherit,
+			btrfs_ioctl_vol_args_v2_u1: btrfs_ioctl_vol_args_v2_u1{
+				//size: 	qgroup_inherit_size(inherit),
+				qgroup_inherit: inherit,
+			},
+		}
+		copy(args.name[:], newName)
+		return iocSubvolCreateV2(dst, &args)
+	}
+	var args btrfs_ioctl_vol_args
+	copy(args.name[:], newName)
+	return iocSubvolCreate(dst, &args)
+}
+
+func DeleteSubVolume(path string) error {
+	if ok, err := IsSubVolume(path); err != nil {
+		return err
+	} else if !ok {
+		return fmt.Errorf("not a subvolume: %s", path)
+	}
+	cpath, err := filepath.Abs(path)
+	if err != nil {
+		return err
+	}
+	dname := filepath.Dir(cpath)
+	vname := filepath.Base(cpath)
+
+	dir, err := openDir(dname)
+	if err != nil {
+		return err
+	}
+	defer dir.Close()
+	var args btrfs_ioctl_vol_args
+	copy(args.name[:], vname)
+	return iocSnapDestroy(dir, &args)
+}
+
+func SnapshotSubVolume(subvol, dst string, ro bool) error {
+	if ok, err := IsSubVolume(subvol); err != nil {
+		return err
+	} else if !ok {
+		return fmt.Errorf("not a subvolume: %s", subvol)
+	}
+	exists := false
+	if st, err := os.Stat(dst); err != nil && !os.IsNotExist(err) {
+		return err
+	} else if err == nil {
+		if !st.IsDir() {
+			return fmt.Errorf("'%s' exists and it is not a directory", dst)
+		}
+		exists = true
+	}
+	var (
+		newName string
+		dstDir  string
+	)
+	if exists {
+		newName = filepath.Base(subvol)
+		dstDir = dst
+	} else {
+		newName = filepath.Base(dst)
+		dstDir = filepath.Dir(dst)
+	}
+	if !checkSubVolumeName(newName) {
+		return fmt.Errorf("invalid snapshot name '%s'", newName)
+	} else if len(newName) >= volNameMax {
+		return fmt.Errorf("snapshot name too long '%s'", newName)
+	}
+	fdst, err := openDir(dstDir)
+	if err != nil {
+		return err
+	}
+	// TODO: make SnapshotSubVolume a method on FS to use existing fd
+	f, err := openDir(subvol)
+	if err != nil {
+		return err
+	}
+	args := btrfs_ioctl_vol_args_v2{
+		fd: int64(f.Fd()),
+	}
+	if ro {
+		args.flags |= subvolReadOnly
+	}
+	// TODO
+	//if inherit != nil {
+	//	args.flags |= subvolQGroupInherit
+	//	args.size = qgroup_inherit_size(inherit)
+	//	args.qgroup_inherit = inherit
+	//}
+	copy(args.name[:], newName)
+	return iocSnapCreateV2(fdst, &args)
 }
diff --git a/utils.go b/utils.go
new file mode 100644
index 0000000..9a6fe0d
--- /dev/null
+++ b/utils.go
@@ -0,0 +1,37 @@
+package btrfs
+
+import (
+	"fmt"
+	"os"
+	"syscall"
+)
+
+func isBtrfs(path string) (bool, error) {
+	var stfs syscall.Statfs_t
+	if err := syscall.Statfs(path, &stfs); err != nil {
+		return false, err
+	}
+	return stfs.Type == SuperMagic, nil
+}
+
+// openDir does the following checks before calling Open:
+// 1: path is in a btrfs filesystem
+// 2: path is a directory
+func openDir(path string) (*os.File, error) {
+	if ok, err := isBtrfs(path); err != nil {
+		return nil, err
+	} else if !ok {
+		return nil, ErrNotBtrfs{Path: path}
+	}
+	file, err := os.Open(path)
+	if err != nil {
+		return nil, err
+	} else if st, err := file.Stat(); err != nil {
+		file.Close()
+		return nil, err
+	} else if !st.IsDir() {
+		file.Close()
+		return nil, fmt.Errorf("not a directory: %s", path)
+	}
+	return file, nil
+}