diff options
author | David Gwynne <dlg@cvs.openbsd.org> | 2008-12-03 23:39:33 +0000 |
---|---|---|
committer | David Gwynne <dlg@cvs.openbsd.org> | 2008-12-03 23:39:33 +0000 |
commit | 6cd9762e49e7e88ad61f8e32d52133aa0a642639 (patch) | |
tree | 3ce7a0c6946e6f49c7794a7c3e128b282741d26d | |
parent | b2b1b8bb0d677b8a82c30123fef6213fe5e7e5c6 (diff) |
add vscsi(4), a virtual scsi controller that passes all scsi requests up to
userland for handling. this is to scsi what tun(4) is for networks.
this is going into the tree so i can work on some crazy scsi stuff, but its
not being enabled since it is useless unless you're working on some crazy
scsi stuff.
-rw-r--r-- | sys/conf/files | 7 | ||||
-rw-r--r-- | sys/dev/vscsi.c | 568 | ||||
-rw-r--r-- | sys/dev/vscsivar.h | 63 | ||||
-rw-r--r-- | sys/kern/init_main.c | 6 | ||||
-rw-r--r-- | sys/sys/conf.h | 11 |
5 files changed, 652 insertions, 3 deletions
diff --git a/sys/conf/files b/sys/conf/files index 14ae228f121..37871a15781 100644 --- a/sys/conf/files +++ b/sys/conf/files @@ -1,4 +1,4 @@ -# $OpenBSD: files,v 1.449 2008/11/24 04:46:59 tedu Exp $ +# $OpenBSD: files,v 1.450 2008/12/03 23:39:32 dlg Exp $ # $NetBSD: files,v 1.87 1996/05/19 17:17:50 jonathan Exp $ # @(#)files.newconf 7.5 (Berkeley) 5/10/93 @@ -444,6 +444,11 @@ file dev/ipmi.c ipmi needs-flag device vmt file dev/vmt.c vmt needs-flag +# Virtual SCSI +device vscsi: scsi +attach vscsi at root +file dev/vscsi.c vscsi needs-flag + # Software RAID device softraid: scsi attach softraid at root diff --git a/sys/dev/vscsi.c b/sys/dev/vscsi.c new file mode 100644 index 00000000000..baac8d343f4 --- /dev/null +++ b/sys/dev/vscsi.c @@ -0,0 +1,568 @@ +/* $OpenBSD: vscsi.c,v 1.1 2008/12/03 23:39:32 dlg Exp $ */ + +/* + * Copyright (c) 2008 David Gwynne <dlg@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include <sys/param.h> +#include <sys/systm.h> +#include <sys/buf.h> +#include <sys/kernel.h> +#include <sys/malloc.h> +#include <sys/device.h> +#include <sys/proc.h> +#include <sys/conf.h> +#include <sys/queue.h> +#include <sys/rwlock.h> +#include <sys/pool.h> +#include <sys/ioctl.h> +#include <sys/poll.h> +#include <sys/selinfo.h> + +#include <scsi/scsi_all.h> +#include <scsi/scsiconf.h> + +#include <dev/vscsivar.h> + +#ifdef VSCSI_DEBUG +#define VSCSI_D_INIT (1<<0) + +int vscsidebug = 0; + +#define DPRINTF(_m, _p...) do { \ + if (ISSET(vscsidebug, (_m))) \ + printf(p); \ + } while (0) +#else +#define DPRINTF(_m, _p...) /* _m, _p */ +#endif + +int vscsi_match(struct device *, void *, void *); +void vscsi_attach(struct device *, struct device *, void *); +void vscsi_shutdown(void *); + +struct vscsi_ccb { + TAILQ_ENTRY(vscsi_ccb) ccb_entry; + int ccb_tag; + struct scsi_xfer *ccb_xs; + size_t ccb_datalen; +}; + +TAILQ_HEAD(vscsi_ccb_list, vscsi_ccb); + +struct vscsi_softc { + struct device sc_dev; + struct scsi_link sc_link; + struct scsibus_softc *sc_scsibus; + + struct pool sc_ccb_pool; + struct vscsi_ccb_list sc_ccb_i2t; + struct vscsi_ccb_list sc_ccb_t2i; + int sc_ccb_tag; + struct mutex sc_ccb_mtx; + struct rwlock sc_ccb_polling; + + struct selinfo sc_sel; + struct mutex sc_sel_mtx; + + struct rwlock sc_open; + volatile int sc_opened; +}; + +#define DEVNAME(_s) ((_s)->sc_dev.dv_xname) +#define DEV2SC(_d) ((struct vscsi_softc *)device_lookup(&vscsi_cd, minor(_d))) + +struct cfattach vscsi_ca = { + sizeof(struct vscsi_softc), + vscsi_match, + vscsi_attach +}; + +struct cfdriver vscsi_cd = { + NULL, + "vscsi", + DV_DULL +}; + +int vscsi_cmd(struct scsi_xfer *); +int vscsi_probe(struct scsi_link *); + +struct scsi_adapter vscsi_switch = { + vscsi_cmd, + minphys, + vscsi_probe, + NULL +}; + +struct scsi_device vscsi_dev = { + NULL, NULL, NULL, NULL +}; + +void vscsi_xs_stuffup(struct scsi_xfer *); + + +int vscsi_i2t(struct vscsi_softc *, struct vscsi_ioc_i2t *); +int vscsi_data(struct vscsi_softc *, struct vscsi_ioc_data *, int); +int vscsi_t2i(struct vscsi_softc *, struct vscsi_ioc_t2i *); + +struct vscsi_ccb * vscsi_ccb_get(struct vscsi_softc *, int); +#define vscsi_ccb_put(_s, _c) pool_put(&(_s)->sc_ccb_pool, (_c)) + +void filt_vscsidetach(struct knote *); +int filt_vscsiread(struct knote *, long); + +struct filterops vscsi_filtops = { + 1, + NULL, + filt_vscsidetach, + filt_vscsiread +}; + + +int +vscsi_match(struct device *parent, void *match, void *aux) +{ + return (1); +} + +void +vscsi_attach(struct device *parent, struct device *self, void *aux) +{ + struct vscsi_softc *sc = (struct vscsi_softc *)self; + struct scsibus_attach_args saa; + + printf("\n"); + + rw_init(&sc->sc_open, DEVNAME(sc)); + rw_init(&sc->sc_ccb_polling, DEVNAME(sc)); + + sc->sc_link.device = &vscsi_dev; + sc->sc_link.adapter = &vscsi_switch; + sc->sc_link.adapter_softc = sc; + sc->sc_link.adapter_target = 256; + sc->sc_link.adapter_buswidth = 256; + sc->sc_link.openings = 1; + + bzero(&saa, sizeof(saa)); + saa.saa_sc_link = &sc->sc_link; + + sc->sc_scsibus = (struct scsibus_softc *)config_found(&sc->sc_dev, + &saa, scsiprint); +} + +int +vscsi_cmd(struct scsi_xfer *xs) +{ + struct scsi_link *link = xs->sc_link; + struct vscsi_softc *sc = link->adapter_softc; + struct vscsi_ccb *ccb; + int polled = ISSET(xs->flags, SCSI_POLL); + + if (sc->sc_opened == 0) { + vscsi_xs_stuffup(xs); + return (COMPLETE); + } + + if (ISSET(xs->flags, SCSI_POLL) && ISSET(xs->flags, SCSI_NOSLEEP)) { + printf("%s: POLL && NOSLEEP for 0x%02x\n", DEVNAME(sc), + xs->cmd->opcode); + vscsi_xs_stuffup(xs); + return (COMPLETE); + } + + ccb = vscsi_ccb_get(sc, ISSET(xs->flags, SCSI_NOSLEEP) ? 0 : 1); + if (ccb == NULL) { + vscsi_xs_stuffup(xs); + return (COMPLETE); + } + + ccb->ccb_xs = xs; + mtx_enter(&sc->sc_ccb_mtx); + TAILQ_INSERT_TAIL(&sc->sc_ccb_i2t, ccb, ccb_entry); + mtx_leave(&sc->sc_ccb_mtx); + + selwakeup(&sc->sc_sel); + KNOTE(&sc->sc_sel.si_note, 0); + + if (polled) { + rw_enter_read(&sc->sc_ccb_polling); + while (ccb->ccb_xs != NULL) + tsleep(ccb, PRIBIO, "vscsipoll", 0); + vscsi_ccb_put(sc, ccb); + rw_exit_read(&sc->sc_ccb_polling); + return (COMPLETE); + } + + return (SUCCESSFULLY_QUEUED); +} + +void +vscsi_xs_stuffup(struct scsi_xfer *xs) +{ + int s; + + xs->error = XS_DRIVER_STUFFUP; + xs->flags |= ITSDONE; + s = splbio(); + scsi_done(xs); + splx(s); +} + +int +vscsi_probe(struct scsi_link *link) +{ + struct vscsi_softc *sc = link->adapter_softc; + + if (sc->sc_opened == 0) + return (ENXIO); + + return (0); +} + +int +vscsiopen(dev_t dev, int flags, int mode, struct proc *p) +{ + struct vscsi_softc *sc = DEV2SC(dev); + int rv; + + if (sc == NULL) + return (ENXIO); + + rv = rw_enter(&sc->sc_open, RW_WRITE | RW_NOSLEEP); + if (rv != 0) + return (rv); + + pool_init(&sc->sc_ccb_pool, sizeof(struct vscsi_ccb), 0, 0, 0, + "vscsiccb", NULL); + pool_setipl(&sc->sc_ccb_pool, IPL_BIO); + TAILQ_INIT(&sc->sc_ccb_i2t); + TAILQ_INIT(&sc->sc_ccb_t2i); + mtx_init(&sc->sc_ccb_mtx, IPL_BIO); + mtx_init(&sc->sc_sel_mtx, IPL_BIO); + + sc->sc_opened = 1; + + return (0); +} + +int +vscsiioctl(dev_t dev, u_long cmd, caddr_t addr, int flags, struct proc *p) +{ + struct vscsi_softc *sc = DEV2SC(dev); + int read = 0; + int err = 0; + + switch (cmd) { + case VSCSI_I2T: + err = vscsi_i2t(sc, (struct vscsi_ioc_i2t *)addr); + break; + + case VSCSI_DATA_READ: + read = 1; + case VSCSI_DATA_WRITE: + err = vscsi_data(sc, (struct vscsi_ioc_data *)addr, read); + break; + + case VSCSI_T2I: + err = vscsi_t2i(sc, (struct vscsi_ioc_t2i *)addr); + break; + + default: + err = ENOTTY; + break; + } + + return (err); +} + +int +vscsi_i2t(struct vscsi_softc *sc, struct vscsi_ioc_i2t *i2t) +{ + struct vscsi_ccb *ccb; + struct scsi_xfer *xs; + struct scsi_link *link; + + mtx_enter(&sc->sc_ccb_mtx); + ccb = TAILQ_FIRST(&sc->sc_ccb_i2t); + if (ccb != NULL) + TAILQ_REMOVE(&sc->sc_ccb_i2t, ccb, ccb_entry); + mtx_leave(&sc->sc_ccb_mtx); + + if (ccb == NULL) + return (EAGAIN); + + xs = ccb->ccb_xs; + link = xs->sc_link; + + i2t->tag = ccb->ccb_tag; + i2t->target = link->target; + i2t->lun = link->lun; + bcopy(xs->cmd, &i2t->cmd, xs->cmdlen); + i2t->cmdlen = xs->cmdlen; + i2t->datalen = xs->datalen; + + switch (xs->flags & (SCSI_DATA_IN | SCSI_DATA_OUT)) { + case SCSI_DATA_IN: + i2t->direction = VSCSI_DIR_READ; + break; + case SCSI_DATA_OUT: + i2t->direction = VSCSI_DIR_WRITE; + break; + default: + i2t->direction = VSCSI_DIR_NONE; + break; + } + + TAILQ_INSERT_TAIL(&sc->sc_ccb_t2i, ccb, ccb_entry); + + return (0); +} + +int +vscsi_data(struct vscsi_softc *sc, struct vscsi_ioc_data *data, int read) +{ + struct vscsi_ccb *ccb; + struct scsi_xfer *xs; + int xsread; + u_int8_t *buf; + int rv = EINVAL; + + TAILQ_FOREACH(ccb, &sc->sc_ccb_t2i, ccb_entry) { + if (ccb->ccb_tag == data->tag) + break; + } + if (ccb == NULL) + return (EFAULT); + + xs = ccb->ccb_xs; + + if (data->datalen + ccb->ccb_datalen > xs->datalen) + return (ENOMEM); + + switch (xs->flags & (SCSI_DATA_IN | SCSI_DATA_OUT)) { + case SCSI_DATA_IN: + xsread = 1; + break; + case SCSI_DATA_OUT: + xsread = 0; + break; + default: + return (EINVAL); + } + + if (read != xsread) + return (EINVAL); + + buf = xs->data; + buf += ccb->ccb_datalen; + + if (read) + rv = copyin(data->data, buf, data->datalen); + else + rv = copyout(buf, data->data, data->datalen); + + if (rv == 0) + ccb->ccb_datalen += data->datalen; + + return (rv); +} + +int +vscsi_t2i(struct vscsi_softc *sc, struct vscsi_ioc_t2i *t2i) +{ + struct vscsi_ccb *ccb; + struct scsi_xfer *xs; + struct scsi_link *link; + int rv = 0; + int polled; + int s; + + TAILQ_FOREACH(ccb, &sc->sc_ccb_t2i, ccb_entry) { + if (ccb->ccb_tag == t2i->tag) + break; + } + if (ccb == NULL) + return (EFAULT); + + TAILQ_REMOVE(&sc->sc_ccb_t2i, ccb, ccb_entry); + + xs = ccb->ccb_xs; + link = xs->sc_link; + + xs->resid = xs->datalen - ccb->ccb_datalen; + xs->status = SCSI_OK; + + switch (t2i->status) { + case VSCSI_STAT_DONE: + xs->error = XS_NOERROR; + break; + case VSCSI_STAT_SENSE: + xs->error = XS_SENSE; + bcopy(&t2i->sense, &xs->sense, t2i->senselen); + xs->req_sense_length = t2i->senselen; + break; + case VSCSI_STAT_ERR: + default: + xs->error = XS_DRIVER_STUFFUP; + break; + } + + polled = ISSET(xs->flags, SCSI_POLL); + + xs->flags |= ITSDONE; + s = splbio(); + scsi_done(xs); + splx(s); + + if (polled) { + ccb->ccb_xs = NULL; + wakeup(ccb); + } else + vscsi_ccb_put(sc, ccb); + + return (rv); +} + +int +vscsipoll(dev_t dev, int events, struct proc *p) +{ + struct vscsi_softc *sc = DEV2SC(dev); + int revents = 0; + + if (events & (POLLIN | POLLRDNORM)) { + mtx_enter(&sc->sc_ccb_mtx); + if (!TAILQ_EMPTY(&sc->sc_ccb_i2t)) + revents |= events & (POLLIN | POLLRDNORM); + mtx_leave(&sc->sc_ccb_mtx); + } + + if (revents == 0) { + if (events & (POLLIN | POLLRDNORM)) + selrecord(p, &sc->sc_sel); + } + + return (revents); +} + +int +vscsikqfilter(dev_t dev, struct knote *kn) +{ + struct vscsi_softc *sc = DEV2SC(dev); + struct klist *klist = &sc->sc_sel.si_note; + + switch (kn->kn_filter) { + case EVFILT_READ: + kn->kn_fop = &vscsi_filtops; + break; + default: + return (1); + } + + kn->kn_hook = (caddr_t)sc; + + mtx_enter(&sc->sc_sel_mtx); + SLIST_INSERT_HEAD(klist, kn, kn_selnext); + mtx_leave(&sc->sc_sel_mtx); + + return (0); +} + +void +filt_vscsidetach(struct knote *kn) +{ + struct vscsi_softc *sc = (struct vscsi_softc *)kn->kn_hook; + struct klist *klist = &sc->sc_sel.si_note; + + mtx_enter(&sc->sc_sel_mtx); + SLIST_REMOVE(klist, kn, knote, kn_selnext); + mtx_leave(&sc->sc_sel_mtx); +} + +int +filt_vscsiread(struct knote *kn, long hint) +{ + struct vscsi_softc *sc = (struct vscsi_softc *)kn->kn_hook; + int event = 0; + + mtx_enter(&sc->sc_ccb_mtx); + if (!TAILQ_EMPTY(&sc->sc_ccb_i2t)) + event = 1; + mtx_leave(&sc->sc_ccb_mtx); + + return (event); +} + +int +vscsiclose(dev_t dev, int flags, int mode, struct proc *p) +{ + struct vscsi_softc *sc = DEV2SC(dev); + struct vscsi_ccb *ccb; + int polled; + int i; + + sc->sc_opened = 0; + + while ((ccb = TAILQ_FIRST(&sc->sc_ccb_t2i)) != NULL) { + TAILQ_REMOVE(&sc->sc_ccb_i2t, ccb, ccb_entry); + polled = ISSET(ccb->ccb_xs->flags, SCSI_POLL); + + vscsi_xs_stuffup(ccb->ccb_xs); + + if (polled) { + ccb->ccb_xs = NULL; + wakeup(ccb); + } else + vscsi_ccb_put(sc, ccb); + } + + while ((ccb = TAILQ_FIRST(&sc->sc_ccb_i2t)) != NULL) { + TAILQ_REMOVE(&sc->sc_ccb_i2t, ccb, ccb_entry); + polled = ISSET(ccb->ccb_xs->flags, SCSI_POLL); + + vscsi_xs_stuffup(ccb->ccb_xs); + + if (polled) { + ccb->ccb_xs = NULL; + wakeup(ccb); + } else + vscsi_ccb_put(sc, ccb); + } + + rw_enter_write(&sc->sc_ccb_polling); + pool_destroy(&sc->sc_ccb_pool); + rw_exit_write(&sc->sc_ccb_polling); + + for (i = 0; i < sc->sc_link.adapter_buswidth; i++) + scsi_detach_target(sc->sc_scsibus, i, DETACH_FORCE); + + rw_exit(&sc->sc_open); + + return (0); +} + +struct vscsi_ccb * +vscsi_ccb_get(struct vscsi_softc *sc, int waitok) +{ + struct vscsi_ccb *ccb; + + ccb = pool_get(&sc->sc_ccb_pool, waitok ? PR_WAITOK : PR_NOWAIT); + if (ccb == NULL) + return (NULL); + + ccb->ccb_tag = sc->sc_ccb_tag++; + ccb->ccb_datalen = 0; + + return (ccb); +} diff --git a/sys/dev/vscsivar.h b/sys/dev/vscsivar.h new file mode 100644 index 00000000000..143805e8c96 --- /dev/null +++ b/sys/dev/vscsivar.h @@ -0,0 +1,63 @@ +/* $OpenBSD: vscsivar.h,v 1.1 2008/12/03 23:39:32 dlg Exp $ */ + +/* + * Copyright (c) 2008 David Gwynne <dlg@openbsd.org> + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef _SYS_DEV_VSCSIVAR_H +#define _SYS_DEV_VSCSIVAR_H + +struct vscsi_ioc_i2t { + int tag; + + u_int target; + u_int lun; + + struct scsi_generic cmd; + size_t cmdlen; + + size_t datalen; + int direction; +#define VSCSI_DIR_NONE 0 +#define VSCSI_DIR_READ 1 +#define VSCSI_DIR_WRITE 2 +}; + +#define VSCSI_I2T _IOR('I', 0, struct vscsi_ioc_i2t) + +struct vscsi_ioc_data { + int tag; + + void * data; + size_t datalen; +}; + +#define VSCSI_DATA_READ _IOW('I', 1, struct vscsi_ioc_data) +#define VSCSI_DATA_WRITE _IOW('I', 2, struct vscsi_ioc_data) + +struct vscsi_ioc_t2i { + int tag; + + int status; +#define VSCSI_STAT_DONE 0 +#define VSCSI_STAT_SENSE 1 +#define VSCSI_STAT_ERR 2 + struct scsi_sense_data sense; + size_t senselen; +}; + +#define VSCSI_T2I _IOW('I', 3, struct vscsi_ioc_t2i) + +#endif /* _SYS_DEV_VSCSIVAR_H */ diff --git a/sys/kern/init_main.c b/sys/kern/init_main.c index 0226b96b3e1..5f8ec54df53 100644 --- a/sys/kern/init_main.c +++ b/sys/kern/init_main.c @@ -1,4 +1,4 @@ -/* $OpenBSD: init_main.c,v 1.154 2008/10/15 23:23:51 deraadt Exp $ */ +/* $OpenBSD: init_main.c,v 1.155 2008/12/03 23:39:32 dlg Exp $ */ /* $NetBSD: init_main.c,v 1.84.4.1 1996/06/02 09:08:06 mrg Exp $ */ /* @@ -98,6 +98,7 @@ extern void nfs_init(void); #endif +#include "vscsi.h" #include "softraid.h" const char copyright[] = @@ -440,6 +441,9 @@ main(void *framep) dostartuphooks(); +#if NVSCSI > 0 + config_rootfound("vscsi", NULL); +#endif #if NSOFTRAID > 0 config_rootfound("softraid", NULL); #endif diff --git a/sys/sys/conf.h b/sys/sys/conf.h index 7b228231ae2..cbdbdfb4738 100644 --- a/sys/sys/conf.h +++ b/sys/sys/conf.h @@ -1,4 +1,4 @@ -/* $OpenBSD: conf.h,v 1.91 2008/11/17 00:40:04 oga Exp $ */ +/* $OpenBSD: conf.h,v 1.92 2008/12/03 23:39:32 dlg Exp $ */ /* $NetBSD: conf.h,v 1.33 1996/05/03 20:03:32 christos Exp $ */ /*- @@ -305,6 +305,14 @@ extern struct cdevsw cdevsw[]; 0, dev_init(c,n,poll), (dev_type_mmap((*))) enodev, \ 0, D_KQFILTER, dev_init(c,n,kqfilter) } +/* open, close, ioctl, poll, kqfilter -- XXX should be generic device */ +#define cdev_vscsi_init(c,n) { \ + dev_init(c,n,open), dev_init(c,n,close), \ + (dev_type_read((*))) enodev, (dev_type_write((*))) enodev, \ + dev_init(c,n,ioctl), (dev_type_stop((*))) enodev, \ + 0, dev_init(c,n,poll), (dev_type_mmap((*))) enodev, \ + 0, D_KQFILTER, dev_init(c,n,kqfilter) } + /* open, close, read, write, ioctl, poll, kqfilter, cloning -- XXX should be generic device */ #define cdev_bpf_init(c,n) { \ dev_init(c,n,open), dev_init(c,n,close), dev_init(c,n,read), \ @@ -666,6 +674,7 @@ cdev_decl(crypto); cdev_decl(systrace); cdev_decl(bio); +cdev_decl(vscsi); cdev_decl(bthub); cdev_decl(gpr); |