diff --git a/.github/workflows/build-test.sh b/.github/workflows/build-test.sh index 44f5b1ae..710c1745 100755 --- a/.github/workflows/build-test.sh +++ b/.github/workflows/build-test.sh @@ -6,6 +6,6 @@ echo 'set -eu' > test.sh for p in $(go list ./...); do dir=".${p#github.com/ncruces/go-sqlite3}" name="$(basename "$p").test" - (cd ${dir}; go test -c) - [ -f "${dir}/${name}" ] && echo "(cd ${dir}; ./${name} ${TESTFLAGS})" >> test.sh + (cd ${dir}; go test -c ${BUILDFLAGS:-}) + [ -f "${dir}/${name}" ] && echo "(cd ${dir}; ./${name} ${TESTFLAGS:-})" >> test.sh done \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index fda82a8c..71705c36 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -52,17 +52,17 @@ jobs: - name: Test run: go test -v ./... -bench . -benchtime=1x + - name: Test no locks + run: go test -v -tags sqlite3_nosys ./... + if: matrix.os == 'ubuntu-latest' + - name: Test BSD locks run: go test -v -tags sqlite3_flock ./... if: matrix.os == 'macos-latest' - name: Test dot locks run: go test -v -tags sqlite3_dotlk ./... - if: matrix.os == 'macos-latest' - - - name: Test no locks - run: go test -v -tags sqlite3_nosys ./... - if: matrix.os == 'ubuntu-latest' + if: matrix.os != 'windows-latest' - name: Test GORM shell: bash @@ -194,6 +194,7 @@ jobs: env: GOOS: solaris TESTFLAGS: '-test.v -test.short' + BUILDFLAGS: '-tags sqlite3_dotlk' run: .github/workflows/build-test.sh - name: Test Solaris diff --git a/internal/testcfg/testcfg.go b/internal/testcfg/testcfg.go index fb5cbdc3..e5acfb7c 100644 --- a/internal/testcfg/testcfg.go +++ b/internal/testcfg/testcfg.go @@ -19,7 +19,8 @@ func init() { path := filepath.Join(os.TempDir(), "wazero") if err := os.MkdirAll(path, 0777); err == nil { if cache, err := wazero.NewCompilationCacheWithDir(path); err == nil { - sqlite3.RuntimeConfig.WithCompilationCache(cache) + sqlite3.RuntimeConfig = sqlite3.RuntimeConfig. + WithCompilationCache(cache) } } } diff --git a/internal/util/mmap.go b/internal/util/mmap.go index 25d19362..613bb90b 100644 --- a/internal/util/mmap.go +++ b/internal/util/mmap.go @@ -55,10 +55,10 @@ type MappedRegion struct { used bool } -func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32, prot int) (*MappedRegion, error) { +func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32, readOnly bool) (*MappedRegion, error) { s := ctx.Value(moduleKey{}).(*moduleState) r := s.new(ctx, mod, size) - err := r.mmap(f, offset, prot) + err := r.mmap(f, offset, readOnly) if err != nil { return nil, err } @@ -75,7 +75,11 @@ func (r *MappedRegion) Unmap() error { return err } -func (r *MappedRegion) mmap(f *os.File, offset int64, prot int) error { +func (r *MappedRegion) mmap(f *os.File, offset int64, readOnly bool) error { + prot := unix.PROT_READ + if !readOnly { + prot |= unix.PROT_WRITE + } _, err := unix.MmapPtr(int(f.Fd()), offset, r.addr, uintptr(r.size), prot, unix.MAP_SHARED|unix.MAP_FIXED) r.used = err == nil diff --git a/internal/util/mmap_windows.go b/internal/util/mmap_windows.go new file mode 100644 index 00000000..fdf6f439 --- /dev/null +++ b/internal/util/mmap_windows.go @@ -0,0 +1,53 @@ +//go:build !sqlite3_nosys + +package util + +import ( + "context" + "os" + "reflect" + "unsafe" + + "github.com/tetratelabs/wazero/api" + "golang.org/x/sys/windows" +) + +type MappedRegion struct { + windows.Handle + Data []byte + addr uintptr +} + +func MapRegion(ctx context.Context, mod api.Module, f *os.File, offset int64, size int32) (*MappedRegion, error) { + h, err := windows.CreateFileMapping(windows.Handle(f.Fd()), nil, windows.PAGE_READWRITE, 0, 0, nil) + if h == 0 { + return nil, err + } + + a, err := windows.MapViewOfFile(h, windows.FILE_MAP_WRITE, + uint32(offset>>32), uint32(offset), uintptr(size)) + if a == 0 { + windows.CloseHandle(h) + return nil, err + } + + res := &MappedRegion{Handle: h, addr: a} + // SliceHeader, although deprecated, avoids a go vet warning. + sh := (*reflect.SliceHeader)(unsafe.Pointer(&res.Data)) + sh.Len = int(size) + sh.Cap = int(size) + sh.Data = a + return res, nil +} + +func (r *MappedRegion) Unmap() error { + if r.Data == nil { + return nil + } + err := windows.UnmapViewOfFile(r.addr) + if err != nil { + return err + } + r.Data = nil + return windows.CloseHandle(r.Handle) +} diff --git a/vfs/shm.go b/vfs/shm.go index 9affac05..9d9dff1c 100644 --- a/vfs/shm.go +++ b/vfs/shm.go @@ -1,4 +1,4 @@ -//go:build ((darwin || linux || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk +//go:build ((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk package vfs @@ -22,8 +22,5 @@ func NewSharedMemory(path string, flags OpenFlag) SharedMemory { if flags&OPEN_MAIN_DB == 0 || flags&(OPEN_DELETEONCLOSE|OPEN_MEMORY) != 0 { return nil } - return &vfsShm{ - path: path, - readOnly: flags&OPEN_READONLY != 0, - } + return &vfsShm{path: path} } diff --git a/vfs/shm_bsd.go b/vfs/shm_bsd.go index a093cc42..d4e04636 100644 --- a/vfs/shm_bsd.go +++ b/vfs/shm_bsd.go @@ -18,11 +18,9 @@ type vfsShmFile struct { *os.File info os.FileInfo - // +checklocks:vfsShmFilesMtx - refs int + refs int // +checklocks:vfsShmFilesMtx - // +checklocks:Mutex - lock [_SHM_NLOCK]int16 + lock [_SHM_NLOCK]int16 // +checklocks:Mutex sync.Mutex } @@ -34,10 +32,9 @@ var ( type vfsShm struct { *vfsShmFile - path string - lock [_SHM_NLOCK]bool - regions []*util.MappedRegion - readOnly bool + path string + lock [_SHM_NLOCK]bool + regions []*util.MappedRegion } func (s *vfsShm) Close() error { @@ -69,7 +66,7 @@ func (s *vfsShm) Close() error { panic(util.AssertErr()) } -func (s *vfsShm) shmOpen() (rc _ErrorCode) { +func (s *vfsShm) shmOpen() _ErrorCode { if s.vfsShmFile != nil { return _OK } @@ -100,17 +97,13 @@ func (s *vfsShm) shmOpen() (rc _ErrorCode) { } } - // Lock and truncate the file, if not readonly. + // Lock and truncate the file. // The lock is only released by closing the file. - if s.readOnly { - rc = _READONLY_CANTINIT - } else { - if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK { - return rc - } - if err := f.Truncate(0); err != nil { - return _IOERR_SHMOPEN - } + if rc := osLock(f, unix.LOCK_EX|unix.LOCK_NB, _IOERR_LOCK); rc != _OK { + return rc + } + if err := f.Truncate(0); err != nil { + return _IOERR_SHMOPEN } // Add the new shared file. @@ -122,11 +115,11 @@ func (s *vfsShm) shmOpen() (rc _ErrorCode) { for i, g := range vfsShmFiles { if g == nil { vfsShmFiles[i] = s.vfsShmFile - return rc + return _OK } } vfsShmFiles = append(vfsShmFiles, s.vfsShmFile) - return rc + return _OK } func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { @@ -148,25 +141,16 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext if !extend { return 0, _OK } - if s.readOnly || osAllocate(s.File, n) != nil { + if osAllocate(s.File, n) != nil { return 0, _IOERR_SHMSIZE } } - var prot int - if s.readOnly { - prot = unix.PROT_READ - } else { - prot = unix.PROT_READ | unix.PROT_WRITE - } - r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size, prot) + r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size, false) if err != nil { return 0, _IOERR_SHMMAP } s.regions = append(s.regions, r) - if s.readOnly { - return r.Ptr, _READONLY - } return r.Ptr, _OK } diff --git a/vfs/shm_copy.go b/vfs/shm_dotlk.go similarity index 89% rename from vfs/shm_copy.go rename to vfs/shm_dotlk.go index 4285f0ba..03343306 100644 --- a/vfs/shm_copy.go +++ b/vfs/shm_dotlk.go @@ -4,6 +4,9 @@ package vfs import ( "context" + "errors" + "io/fs" + "os" "sync" "unsafe" @@ -11,8 +14,6 @@ import ( "github.com/tetratelabs/wazero/api" ) -const _WALINDEX_PGSZ = 32768 - type vfsShmBuffer struct { shared []byte // +checklocks:Mutex refs int // +checklocks:vfsShmBuffersMtx @@ -29,15 +30,14 @@ var ( type vfsShm struct { *vfsShmBuffer - mod api.Module - alloc api.Function - free api.Function - path string - shadow []byte - ptrs []uint32 - stack [1]uint64 - lock [_SHM_NLOCK]bool - readOnly bool + mod api.Module + alloc api.Function + free api.Function + path string + shadow []byte + ptrs []uint32 + stack [1]uint64 + lock [_SHM_NLOCK]bool } func (s *vfsShm) Close() error { @@ -58,13 +58,18 @@ func (s *vfsShm) Close() error { return nil } + err := os.Remove(s.path) + if err != nil && !errors.Is(err, fs.ErrNotExist) { + return _IOERR_UNLOCK + } delete(vfsShmBuffers, s.path) + s.vfsShmBuffer = nil return nil } -func (s *vfsShm) shmOpen() { +func (s *vfsShm) shmOpen() _ErrorCode { if s.vfsShmBuffer != nil { - return + return _OK } vfsShmBuffersMtx.Lock() @@ -74,12 +79,23 @@ func (s *vfsShm) shmOpen() { if g, ok := vfsShmBuffers[s.path]; ok { s.vfsShmBuffer = g g.refs++ - return + return _OK + } + + // Create a directory on disk to ensure only this process + // uses this path to register a shared memory. + err := os.Mkdir(s.path, 0777) + if errors.Is(err, fs.ErrExist) { + return _BUSY + } + if err != nil { + return _IOERR_LOCK } // Add the new shared buffer. s.vfsShmBuffer = &vfsShmBuffer{} vfsShmBuffers[s.path] = s.vfsShmBuffer + return _OK } func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { @@ -91,8 +107,10 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext s.free = mod.ExportedFunction("sqlite3_free") s.alloc = mod.ExportedFunction("sqlite3_malloc64") } + if rc := s.shmOpen(); rc != _OK { + return 0, rc + } - s.shmOpen() s.Lock() defer s.Unlock() defer s.shmAcquire() @@ -131,7 +149,7 @@ func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { switch { case flags&_SHM_LOCK != 0: - s.shmAcquire() + defer s.shmAcquire() case flags&_SHM_EXCLUSIVE != 0: s.shmRelease() } @@ -228,6 +246,8 @@ func (s *vfsShm) shmBarrier() { // // https://sqlite.org/walformat.html#the_wal_index_file_format +const _WALINDEX_PGSZ = 32768 + // +checklocks:s.Mutex func (s *vfsShm) shmAcquire() { // Copies modified words from shared to private memory. diff --git a/vfs/shm_ofd.go b/vfs/shm_ofd.go index 019fc7ab..75c8fbcf 100644 --- a/vfs/shm_ofd.go +++ b/vfs/shm_ofd.go @@ -1,4 +1,4 @@ -//go:build (darwin || linux) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_flock || sqlite3_dotlk || sqlite3_nosys) +//go:build (linux || darwin) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_flock || sqlite3_dotlk || sqlite3_nosys) package vfs @@ -21,21 +21,20 @@ type vfsShm struct { regions []*util.MappedRegion readOnly bool blocking bool - barrier sync.Mutex + sync.Mutex } var _ blockingSharedMemory = &vfsShm{} func (s *vfsShm) shmOpen() _ErrorCode { if s.File == nil { - var flag int - if s.readOnly { - flag = unix.O_RDONLY - } else { - flag = unix.O_RDWR - } f, err := os.OpenFile(s.path, - flag|unix.O_CREAT|unix.O_NOFOLLOW, 0666) + unix.O_RDWR|unix.O_CREAT|unix.O_NOFOLLOW, 0666) + if err != nil { + f, err = os.OpenFile(s.path, + unix.O_RDONLY|unix.O_CREAT|unix.O_NOFOLLOW, 0666) + s.readOnly = true + } if err != nil { return _CANTOPEN } @@ -65,10 +64,7 @@ func (s *vfsShm) shmOpen() _ErrorCode { return _IOERR_SHMOPEN } } - if rc := osReadLock(s.File, _SHM_DMS, 1, time.Millisecond); rc != _OK { - return rc - } - return _OK + return osReadLock(s.File, _SHM_DMS, 1, time.Millisecond) } func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { @@ -95,13 +91,7 @@ func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, ext } } - var prot int - if s.readOnly { - prot = unix.PROT_READ - } else { - prot = unix.PROT_READ | unix.PROT_WRITE - } - r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size, prot) + r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size, s.readOnly) if err != nil { return 0, _IOERR_SHMMAP } @@ -157,8 +147,7 @@ func (s *vfsShm) shmUnmap(delete bool) { for _, r := range s.regions { r.Unmap() } - clear(s.regions) - s.regions = s.regions[:0] + s.regions = nil // Close the file. if delete { @@ -169,9 +158,9 @@ func (s *vfsShm) shmUnmap(delete bool) { } func (s *vfsShm) shmBarrier() { - s.barrier.Lock() + s.Lock() //lint:ignore SA2001 memory barrier. - s.barrier.Unlock() + s.Unlock() } func (s *vfsShm) shmEnableBlocking(block bool) { diff --git a/vfs/shm_other.go b/vfs/shm_other.go index 9394b626..9602dd0c 100644 --- a/vfs/shm_other.go +++ b/vfs/shm_other.go @@ -1,4 +1,4 @@ -//go:build !(((darwin || linux || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk) +//go:build !(((linux || darwin || windows || freebsd || openbsd || netbsd || dragonfly || illumos) && (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !sqlite3_nosys) || sqlite3_flock || sqlite3_dotlk) package vfs diff --git a/vfs/shm_windows.go b/vfs/shm_windows.go new file mode 100644 index 00000000..a0c240c3 --- /dev/null +++ b/vfs/shm_windows.go @@ -0,0 +1,242 @@ +//go:build (386 || arm || amd64 || arm64 || riscv64 || ppc64le) && !(sqlite3_dotlk || sqlite3_nosys) + +package vfs + +import ( + "context" + "io" + "os" + "sync" + "time" + "unsafe" + + "github.com/tetratelabs/wazero/api" + "golang.org/x/sys/windows" + + "github.com/ncruces/go-sqlite3/internal/util" + "github.com/ncruces/go-sqlite3/util/osutil" +) + +type vfsShm struct { + *os.File + mod api.Module + alloc api.Function + free api.Function + path string + regions []*util.MappedRegion + shared [][]byte + shadow []byte + ptrs []uint32 + stack [1]uint64 + blocking bool + sync.Mutex +} + +var _ blockingSharedMemory = &vfsShm{} + +func (s *vfsShm) Close() error { + // Unmap regions. + for _, r := range s.regions { + r.Unmap() + } + s.regions = nil + + // Close the file. + return s.File.Close() +} + +func (s *vfsShm) shmOpen() _ErrorCode { + if s.File == nil { + f, err := osutil.OpenFile(s.path, os.O_RDWR|os.O_CREATE, 0666) + if err != nil { + return _CANTOPEN + } + s.File = f + } + + // Dead man's switch. + if rc := osWriteLock(s.File, _SHM_DMS, 1, 0); rc == _OK { + err := s.Truncate(0) + osUnlock(s.File, _SHM_DMS, 1) + if err != nil { + return _IOERR_SHMOPEN + } + } + return osReadLock(s.File, _SHM_DMS, 1, time.Millisecond) +} + +func (s *vfsShm) shmMap(ctx context.Context, mod api.Module, id, size int32, extend bool) (uint32, _ErrorCode) { + // Ensure size is a multiple of the OS page size. + if size != _WALINDEX_PGSZ || (windows.Getpagesize()-1)&_WALINDEX_PGSZ != 0 { + return 0, _IOERR_SHMMAP + } + if s.mod == nil { + s.mod = mod + s.free = mod.ExportedFunction("sqlite3_free") + s.alloc = mod.ExportedFunction("sqlite3_malloc64") + } + if rc := s.shmOpen(); rc != _OK { + return 0, rc + } + + defer s.shmAcquire() + + // Check if file is big enough. + o, err := s.Seek(0, io.SeekEnd) + if err != nil { + return 0, _IOERR_SHMSIZE + } + if n := (int64(id) + 1) * int64(size); n > o { + if !extend { + return 0, _OK + } + if osAllocate(s.File, n) != nil { + return 0, _IOERR_SHMSIZE + } + } + + // Map the region into memory. + r, err := util.MapRegion(ctx, mod, s.File, int64(id)*int64(size), size) + if err != nil { + return 0, _IOERR_SHMMAP + } + s.regions = append(s.regions, r) + + if int(id) >= len(s.shared) { + s.shared = append(s.shared, make([][]byte, int(id)-len(s.shared)+1)...) + } + s.shared[id] = r.Data + + // Allocate shadow memory. + if n := (int(id) + 1) * int(size); n > len(s.shadow) { + s.shadow = append(s.shadow, make([]byte, n-len(s.shadow))...) + } + + // Allocate local memory. + for int(id) >= len(s.ptrs) { + s.stack[0] = uint64(size) + if err := s.alloc.CallWithStack(ctx, s.stack[:]); err != nil { + panic(err) + } + if s.stack[0] == 0 { + panic(util.OOMErr) + } + clear(util.View(s.mod, uint32(s.stack[0]), _WALINDEX_PGSZ)) + s.ptrs = append(s.ptrs, uint32(s.stack[0])) + } + + return s.ptrs[id], _OK +} + +func (s *vfsShm) shmLock(offset, n int32, flags _ShmFlag) _ErrorCode { + switch { + case flags&_SHM_LOCK != 0: + defer s.shmAcquire() + case flags&_SHM_EXCLUSIVE != 0: + s.shmRelease() + } + + var timeout time.Duration + if s.blocking { + timeout = time.Millisecond + } + + switch { + case flags&_SHM_UNLOCK != 0: + return osUnlock(s.File, _SHM_BASE+uint32(offset), uint32(n)) + case flags&_SHM_SHARED != 0: + return osReadLock(s.File, _SHM_BASE+uint32(offset), uint32(n), timeout) + case flags&_SHM_EXCLUSIVE != 0: + return osWriteLock(s.File, _SHM_BASE+uint32(offset), uint32(n), timeout) + default: + panic(util.AssertErr()) + } +} + +func (s *vfsShm) shmUnmap(delete bool) { + if s.File == nil { + return + } + + s.shmRelease() + + // Free local memory. + for _, p := range s.ptrs { + s.stack[0] = uint64(p) + if err := s.free.CallWithStack(context.Background(), s.stack[:]); err != nil { + panic(err) + } + } + s.ptrs = nil + s.shadow = nil + s.shared = nil + + // Close the file. + s.Close() + s.File = nil + if delete { + os.Remove(s.path) + } +} + +func (s *vfsShm) shmBarrier() { + s.Lock() + s.shmAcquire() + s.shmRelease() + s.Unlock() +} + +const _WALINDEX_PGSZ = 32768 + +func (s *vfsShm) shmAcquire() { + // Copies modified words from shared to private memory. + for id, p := range s.ptrs { + i0 := id * _WALINDEX_PGSZ + i1 := i0 + _WALINDEX_PGSZ + shared := shmPage(s.shared[id]) + shadow := shmPage(s.shadow[i0:i1]) + privat := shmPage(util.View(s.mod, p, _WALINDEX_PGSZ)) + if shmPageEq(shadow, shared) { + continue + } + for i, shared := range shared { + if shadow[i] != shared { + shadow[i] = shared + privat[i] = shared + } + } + } +} + +func (s *vfsShm) shmRelease() { + // Copies modified words from private to shared memory. + for id, p := range s.ptrs { + i0 := id * _WALINDEX_PGSZ + i1 := i0 + _WALINDEX_PGSZ + shared := shmPage(s.shared[id]) + shadow := shmPage(s.shadow[i0:i1]) + privat := shmPage(util.View(s.mod, p, _WALINDEX_PGSZ)) + if shmPageEq(shadow, privat) { + continue + } + for i, privat := range privat { + if shadow[i] != privat { + shadow[i] = privat + shared[i] = privat + } + } + } +} + +func shmPage(s []byte) *[_WALINDEX_PGSZ / 4]uint32 { + p := (*uint32)(unsafe.Pointer(unsafe.SliceData(s))) + return (*[_WALINDEX_PGSZ / 4]uint32)(unsafe.Slice(p, _WALINDEX_PGSZ/4)) +} + +func shmPageEq(p1, p2 *[_WALINDEX_PGSZ / 4]uint32) bool { + return *(*[_WALINDEX_PGSZ / 8]uint32)(p1[:]) == *(*[_WALINDEX_PGSZ / 8]uint32)(p2[:]) +} + +func (s *vfsShm) shmEnableBlocking(block bool) { + s.blocking = block +} diff --git a/vfs/tests/mptest/mptest_test.go b/vfs/tests/mptest/mptest_test.go index ab44b3b5..53a90fc2 100644 --- a/vfs/tests/mptest/mptest_test.go +++ b/vfs/tests/mptest/mptest_test.go @@ -145,9 +145,6 @@ func Test_crash01(t *testing.T) { if testing.Short() { t.Skip("skipping in short mode") } - if os.Getenv("CI") != "" { - t.Skip("skipping in CI") - } if !vfs.SupportsFileLocking { t.Skip("skipping without locks") } @@ -212,9 +209,6 @@ func Test_crash01_wal(t *testing.T) { if testing.Short() { t.Skip("skipping in short mode") } - if os.Getenv("CI") != "" { - t.Skip("skipping in CI") - } if !vfs.SupportsSharedMemory { t.Skip("skipping without shared memory") }