| /* Copyright (C) 2011 The Android Open Source Project |
| ** |
| ** This software is licensed under the terms of the GNU General Public |
| ** License version 2, as published by the Free Software Foundation, and |
| ** may be copied, distributed, and modified under those terms. |
| ** |
| ** This program is distributed in the hope that it will be useful, |
| ** but WITHOUT ANY WARRANTY; without even the implied warranty of |
| ** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| ** GNU General Public License for more details. |
| */ |
| #include "android/utils/intmap.h" |
| #include "android/utils/panic.h" |
| #include "android/utils/reflist.h" |
| #include "android/utils/system.h" |
| #include "android/hw-qemud.h" |
| #include "hw/goldfish_pipe.h" |
| |
| #define DEBUG 0 |
| |
| #if DEBUG >= 1 |
| # define D(...) fprintf(stderr, __VA_ARGS__), fprintf(stderr, "\n") |
| #else |
| # define D(...) (void)0 |
| #endif |
| |
| #if DEBUG >= 2 |
| # define DD(...) fprintf(stderr, __VA_ARGS__), fprintf(stderr, "\n") |
| #else |
| # define DD(...) (void)0 |
| #endif |
| |
| #define E(...) fprintf(stderr, "ERROR:" __VA_ARGS__), fprintf(stderr, "\n") |
| |
| /* Must match hw/goldfish_trace.h */ |
| #define SLOT_COMMAND 0 |
| #define SLOT_STATUS 0 |
| #define SLOT_ADDRESS 1 |
| #define SLOT_SIZE 2 |
| #define SLOT_CHANNEL 3 |
| |
| #define QEMUD_PIPE_CMD_CLOSE 1 |
| #define QEMUD_PIPE_CMD_SEND 2 |
| #define QEMUD_PIPE_CMD_RECV 3 |
| #define QEMUD_PIPE_CMD_WAKE_ON_SEND 4 |
| #define QEMUD_PIPE_CMD_WAKE_ON_RECV 5 |
| |
| #define QEMUD_PIPE_ERROR_INVAL 22 /* EINVAL */ |
| #define QEMUD_PIPE_ERROR_AGAIN 11 /* EAGAIN */ |
| #define QEMUD_PIPE_ERROR_CONNRESET 104 /* ECONNRESET */ |
| #define QEMUD_PIPE_ERROR_NOMEM 12 /* ENOMEM */ |
| |
| /********************************************************************** |
| ********************************************************************** |
| ** |
| ** TECHNICAL NOTE ON THE FOLLOWING IMPLEMENTATION: |
| ** |
| ** PipeService :: |
| ** The global state for the QEMUD fast pipes service. |
| ** Holds a tid -> ThreadState map. Registers as a Qemud |
| ** service named "fast-pipes" which creates PipeClient |
| ** objects on connection. |
| ** |
| ** PipeClient :: |
| ** The state of each QEMUD pipe. This handles the initial |
| ** connection, message exchanges and signalling. |
| ** |
| ** ThreadState :: |
| ** Hold the thread-specific state corresponding to each guest |
| ** thread that has created a least one qemud pipe. Stores the |
| ** state of our 4 I/O Registers, and a map of localId -> PipeState |
| ** |
| ** |
| ** The following graphics is an example corresponding to the following |
| ** situation: |
| ** |
| ** - two guest threads have opened pipe connections |
| ** - the first thread has opened two different pipes |
| ** - the second thread has opened only one pipe |
| ** |
| ** |
| ** QEMUD-SERVICE |
| ** | | |
| ** | |________________________________________ |
| ** | | | | |
| ** | v v v |
| ** | QEMUD-CLIENT#1 QEMUD-CLIENT#2 QEMUD-CLIENT#3 |
| ** | ^ ^ ^ |
| ** | | | | |
| ** | | | | |
| ** | v v v |
| ** | PIPE-CLIENT#1 PIPE-CLIENT#2 PIPE-CLIENT#3 |
| ** | ^ ^ ^ |
| ** | | | | |
| ** | |________________| | |
| ** | | + |
| ** | | | |
| ** | THREAD-STATE#1 THREAD-STATE#2 |
| ** | ^ ^ |
| ** | ____|___________________________________| |
| ** | | |
| ** | | |
| ** PIPE-SERVICE |
| ** |
| ** Note that the QemudService and QemudClient objects are created by |
| ** hw-qemud.c and not defined here. |
| ** |
| **/ |
| |
| typedef struct PipeService PipeService; |
| typedef struct PipeClient PipeClient; |
| |
| static void pipeService_removeState(PipeService* pipeSvc, int tid); |
| |
| /********************************************************************** |
| ********************************************************************** |
| ***** |
| ***** REGISTRY OF SUPPORTED PIPE SERVICES |
| ***** |
| *****/ |
| |
| typedef struct { |
| const char* pipeName; |
| void* pipeOpaque; |
| const QemudPipeHandlerFuncs* pipeFuncs; |
| } PipeType; |
| |
| #define MAX_PIPE_TYPES 4 |
| |
| static PipeType sPipeTypes[MAX_PIPE_TYPES]; |
| static int sPipeTypeCount; |
| |
| void |
| goldfish_pipe_add_type( const char* pipeName, |
| void* pipeOpaque, |
| const QemudPipeHandlerFuncs* pipeFuncs ) |
| { |
| int count = sPipeTypeCount; |
| |
| if (count >= MAX_PIPE_TYPES) { |
| APANIC("%s: Too many qemud pipe types!", __FUNCTION__); |
| } |
| |
| sPipeTypes[count].pipeName = pipeName; |
| sPipeTypes[count].pipeOpaque = pipeOpaque; |
| sPipeTypes[count].pipeFuncs = pipeFuncs; |
| sPipeTypeCount = ++count; |
| } |
| |
| static const PipeType* |
| goldfish_pipe_find_type( const char* pipeName ) |
| { |
| const PipeType* ptype = sPipeTypes; |
| const PipeType* limit = ptype + sPipeTypeCount; |
| |
| for ( ; ptype < limit; ptype++ ) { |
| if (!strcmp(pipeName, ptype->pipeName)) { |
| return ptype; |
| } |
| } |
| return NULL; |
| } |
| |
| |
| /********************************************************************** |
| ********************************************************************** |
| ***** |
| ***** THREAD-SPECIFIC STATE |
| ***** |
| *****/ |
| |
| static void |
| pipeClient_closeFromThread( PipeClient* pcl ); |
| |
| static uint32_t |
| pipeClient_doCommand( PipeClient* pcl, |
| uint32_t command, |
| uint32_t address, |
| uint32_t* pSize ); |
| |
| |
| /* For each guest thread, we will store the following state: |
| * |
| * - The current state of the 'address', 'size', 'localId' and 'status' |
| * I/O slots provided through the magic page by hw/goldfish_trace.c |
| * |
| * - A list of PipeClient objects, corresponding to all the pipes in |
| * this thread, identified by localId. |
| */ |
| typedef struct { |
| uint32_t address; |
| uint32_t size; |
| uint32_t localId; |
| uint32_t status; |
| AIntMap* pipes; |
| } ThreadState; |
| |
| static void |
| threadState_free( ThreadState* ts ) |
| { |
| /* Get rid of the localId -> PipeClient map */ |
| AINTMAP_FOREACH_VALUE(ts->pipes, pcl, pipeClient_closeFromThread(pcl)); |
| aintMap_free(ts->pipes); |
| |
| AFREE(ts); |
| } |
| |
| static ThreadState* |
| threadState_new( void ) |
| { |
| ThreadState* ts; |
| ANEW0(ts); |
| ts->pipes = aintMap_new(); |
| return ts; |
| } |
| |
| static int |
| threadState_addPipe( ThreadState* ts, int localId, PipeClient* pcl ) |
| { |
| /* We shouldn't already have a pipe for this localId */ |
| if (aintMap_get(ts->pipes, localId) != NULL) { |
| errno = EBADF; |
| return -1; |
| } |
| aintMap_set(ts->pipes, localId, pcl); |
| return 0; |
| } |
| |
| static void |
| threadState_delPipe( ThreadState* ts, int localId ) |
| { |
| aintMap_del(ts->pipes, localId); |
| } |
| |
| static void |
| threadState_write( ThreadState* ts, int offset, uint32_t value ) |
| { |
| PipeClient* pcl; |
| |
| switch (offset) { |
| case SLOT_COMMAND: |
| pcl = aintMap_get(ts->pipes, (int)ts->localId); |
| if (pcl == NULL) { |
| D("%s: Invalid localId (%d)", |
| __FUNCTION__, ts->localId); |
| ts->status = QEMUD_PIPE_ERROR_INVAL; |
| } else { |
| ts->status = pipeClient_doCommand(pcl, |
| value, |
| ts->address, |
| &ts->size); |
| } |
| break; |
| case SLOT_ADDRESS: |
| ts->address = value; |
| break; |
| case SLOT_SIZE: |
| ts->size = value; |
| break; |
| case SLOT_CHANNEL: |
| ts->localId = value; |
| break; |
| default: |
| /* XXX: PRINT ERROR? */ |
| ; |
| } |
| } |
| |
| static uint32_t |
| threadState_read( ThreadState* ts, int offset ) |
| { |
| switch (offset) { |
| case SLOT_STATUS: return ts->status; |
| case SLOT_ADDRESS: return ts->address; |
| case SLOT_SIZE: return ts->size; |
| case SLOT_CHANNEL: return ts->localId; |
| default: return 0; |
| } |
| } |
| |
| /********************************************************************** |
| ********************************************************************** |
| ***** |
| ***** PIPE CLIENT STATE |
| ***** |
| *****/ |
| |
| /* Each client object points to a PipeState after it has received the |
| * initial request from the guest, which shall look like: |
| * <tid>:<localId>:<name> |
| */ |
| struct PipeClient { |
| char* pipeName; |
| QemudClient* client; |
| PipeService* pipeSvc; |
| |
| int tid; |
| uint32_t localId; |
| void* handler; |
| const QemudPipeHandlerFuncs* handlerFuncs; |
| }; |
| |
| static int pipeService_addPipe(PipeService* pipeSvc, int tid, int localId, PipeClient* pcl); |
| static void pipeService_removePipe(PipeService* pipeSvc, int tid, int localId); |
| |
| static void |
| pipeClient_closeFromThread( PipeClient* pcl ) |
| { |
| qemud_client_close(pcl->client); |
| } |
| |
| /* This function should only be invoked through qemud_client_close(). |
| * Never call it explicitely. */ |
| static void |
| pipeClient_close( void* opaque ) |
| { |
| PipeClient* pcl = opaque; |
| |
| if (pcl->handler && pcl->handlerFuncs && pcl->handlerFuncs->close) { |
| pcl->handlerFuncs->close(pcl->handler); |
| } |
| |
| D("qemud:pipe: closing client (%d,%x)", pcl->tid, pcl->localId); |
| qemud_client_close(pcl->client); |
| pcl->client = NULL; |
| |
| /* The guest is closing the connection, so remove it from our state */ |
| pipeService_removePipe(pcl->pipeSvc, pcl->tid, pcl->localId); |
| |
| AFREE(pcl->pipeName); |
| } |
| |
| static void |
| pipeClient_recv( void* opaque, uint8_t* msg, int msgLen, QemudClient* client ) |
| { |
| PipeClient* pcl = opaque; |
| const PipeType* ptype; |
| |
| const char* p; |
| int failure = 1; |
| char answer[64]; |
| |
| if (pcl->pipeName != NULL) { |
| /* Should never happen, the only time we'll receive something |
| * is when the connection is created! Simply ignore any incoming |
| * message. |
| */ |
| return; |
| } |
| |
| /* The message format is <tid>:<localIdHex>:<name> */ |
| if (sscanf((char*)msg, "%d:%x:", &pcl->tid, &pcl->localId) != 2) { |
| goto BAD_FORMAT; |
| } |
| p = strchr((const char*)msg, ':'); |
| if (p != NULL) { |
| p = strchr(p+1, ':'); |
| if (p != NULL) |
| p += 1; |
| } |
| if (p == NULL || *p == '\0') { |
| goto BAD_FORMAT; |
| } |
| |
| pcl->pipeName = ASTRDUP(p); |
| |
| ptype = goldfish_pipe_find_type(pcl->pipeName); |
| if (ptype == NULL) { |
| goto UNKNOWN_PIPE_TYPE; |
| } |
| |
| pcl->handlerFuncs = ptype->pipeFuncs; |
| pcl->handler = ptype->pipeFuncs->init( client, ptype->pipeOpaque ); |
| if (pcl->handler == NULL) { |
| goto BAD_INIT; |
| } |
| |
| if (pipeService_addPipe(pcl->pipeSvc, pcl->tid, pcl->localId, pcl) < 0) { |
| goto DUPLICATE_REQUEST; |
| } |
| |
| D("qemud:pipe: Added new client: %s", msg); |
| failure = 0; |
| snprintf(answer, sizeof answer, "OK"); |
| goto SEND_ANSWER; |
| |
| BAD_INIT: |
| /* Initialization failed for some reason! */ |
| E("qemud:pipe: Could not initialize pipe: '%s'", pcl->pipeName); |
| snprintf(answer, sizeof answer, "KO:%d:Could not initialize pipe", |
| QEMUD_PIPE_ERROR_INVAL); |
| goto SEND_ANSWER; |
| |
| UNKNOWN_PIPE_TYPE: |
| E("qemud:pipe: Unknown pipe type: '%s'", p); |
| snprintf(answer, sizeof answer, "KO:%d:Unknown pipe type name", |
| QEMUD_PIPE_ERROR_INVAL); |
| goto SEND_ANSWER; |
| |
| BAD_FORMAT: |
| E("qemud:pipe: Invalid connection request: '%s'", msg); |
| snprintf(answer, sizeof answer, "KO:%d:Invalid connection request", |
| QEMUD_PIPE_ERROR_INVAL); |
| goto SEND_ANSWER; |
| |
| DUPLICATE_REQUEST: |
| E("qemud:pipe: Duplicate connection request: '%s'", msg); |
| snprintf(answer, sizeof answer, "KO:%d:Duplicate connection request", |
| QEMUD_PIPE_ERROR_INVAL); |
| goto SEND_ANSWER; |
| |
| SEND_ANSWER: |
| qemud_client_send(client, (uint8_t*)answer, strlen(answer)); |
| if (failure) { |
| qemud_client_close(client); |
| } else { |
| /* Disable framing for the rest of signalling */ |
| qemud_client_set_framing(client, 0); |
| } |
| } |
| |
| /* Count the number of GoldfishPipeBuffers we will need to transfer |
| * memory from/to [address...address+size) |
| */ |
| static int |
| _countPipeBuffers( uint32_t address, uint32_t size ) |
| { |
| CPUState* env = cpu_single_env; |
| int count = 0; |
| |
| while (size > 0) { |
| uint32_t vstart = address & TARGET_PAGE_MASK; |
| uint32_t vend = vstart + TARGET_PAGE_SIZE; |
| uint32_t next = address + size; |
| |
| DD("%s: trying to map (0x%x - 0x%0x, %d bytes) -> page=0x%x - 0x%x", |
| __FUNCTION__, address, address+size, size, vstart, vend); |
| |
| if (next > vend) { |
| next = vend; |
| } |
| |
| /* Check that the address is valid */ |
| if (cpu_get_phys_page_debug(env, vstart) == -1) { |
| DD("%s: bad guest address!", __FUNCTION__); |
| return -1; |
| } |
| |
| count++; |
| |
| size -= (next - address); |
| address = next; |
| } |
| return count; |
| } |
| |
| /* Fill the pipe buffers to prepare memory transfer from/to |
| * [address...address+size). This assumes 'buffers' points to an array |
| * which size corresponds to _countPipeBuffers(address, size) |
| */ |
| static void |
| _fillPipeBuffers( uint32_t address, uint32_t size, GoldfishPipeBuffer* buffers ) |
| { |
| CPUState* env = cpu_single_env; |
| |
| while (size > 0) { |
| uint32_t vstart = address & TARGET_PAGE_MASK; |
| uint32_t vend = vstart + TARGET_PAGE_SIZE; |
| uint32_t next = address + size; |
| |
| if (next > vend) { |
| next = vend; |
| } |
| |
| /* Translate virtual address into physical one, into emulator |
| * memory. */ |
| target_phys_addr_t phys = cpu_get_phys_page_debug(env, vstart); |
| |
| buffers[0].data = qemu_get_ram_ptr(phys) + (address - vstart); |
| buffers[0].size = next - address; |
| buffers++; |
| |
| size -= (next - address); |
| address = next; |
| } |
| } |
| |
| static uint32_t |
| pipeClient_doCommand( PipeClient* pcl, |
| uint32_t command, |
| uint32_t address, |
| uint32_t* pSize ) |
| { |
| uint32_t size = *pSize; |
| |
| D("%s: TID=%4d CHANNEL=%08x COMMAND=%d ADDRESS=%08x SIZE=%d\n", |
| __FUNCTION__, pcl->tid, pcl->localId, command, address, size); |
| |
| /* XXX: TODO */ |
| switch (command) { |
| case QEMUD_PIPE_CMD_CLOSE: |
| /* The client is asking us to close the connection, so |
| * just do that. */ |
| qemud_client_close(pcl->client); |
| return 0; |
| |
| case QEMUD_PIPE_CMD_SEND: { |
| /* First, try to allocate a buffer from the handler */ |
| void* opaque = pcl->handler; |
| GoldfishPipeBuffer* buffers; |
| int numBuffers = 0; |
| |
| /* Count the number of buffers we need, allocate them, |
| * then fill them. */ |
| numBuffers = _countPipeBuffers(address, size); |
| if (numBuffers < 0) { |
| D("%s: Invalid guest address range 0x%x - 0x%x (%d bytes)", |
| __FUNCTION__, address, address+size, size); |
| return QEMUD_PIPE_ERROR_NOMEM; |
| } |
| buffers = alloca( sizeof(*buffers) * numBuffers ); |
| _fillPipeBuffers(address, size, buffers); |
| |
| D("%s: Sending %d bytes using %d buffers", __FUNCTION__, |
| size, numBuffers); |
| |
| /* Send the data */ |
| if (pcl->handlerFuncs->sendBuffers(opaque, buffers, numBuffers) < 0) { |
| /* When .sendBuffers() returns -1, it usually means |
| * that the handler isn't ready to accept a new message. There |
| * is however once exception: when the message is too large |
| * and it wasn't possible to allocate the buffer. |
| * |
| * Differentiate between these two cases by looking at errno. |
| */ |
| if (errno == ENOMEM) |
| return QEMUD_PIPE_ERROR_NOMEM; |
| else |
| return QEMUD_PIPE_ERROR_AGAIN; |
| } |
| return 0; |
| } |
| |
| case QEMUD_PIPE_CMD_RECV: { |
| void* opaque = pcl->handler; |
| GoldfishPipeBuffer* buffers; |
| int numBuffers, ret; |
| |
| /* Count the number of buffers we have */ |
| numBuffers = _countPipeBuffers(address, size); |
| if (numBuffers < 0) { |
| D("%s: Invalid guest address range 0x%x - 0x%x (%d bytes)", |
| __FUNCTION__, address, address+size, size); |
| return QEMUD_PIPE_ERROR_NOMEM; |
| } |
| buffers = alloca(sizeof(*buffers)*numBuffers); |
| _fillPipeBuffers(address, size, buffers); |
| |
| /* Receive data */ |
| ret = pcl->handlerFuncs->recvBuffers(opaque, buffers, numBuffers); |
| if (ret < 0) { |
| if (errno == ENOMEM) { |
| // XXXX: TODO *pSize = msgSize; |
| return QEMUD_PIPE_ERROR_NOMEM; |
| } else { |
| return QEMUD_PIPE_ERROR_AGAIN; |
| } |
| } |
| *pSize = ret; |
| return 0; |
| } |
| |
| case QEMUD_PIPE_CMD_WAKE_ON_SEND: { |
| pcl->handlerFuncs->wakeOn(pcl->handler, QEMUD_PIPE_WAKE_ON_SEND); |
| return 0; |
| } |
| |
| case QEMUD_PIPE_CMD_WAKE_ON_RECV: { |
| pcl->handlerFuncs->wakeOn(pcl->handler, QEMUD_PIPE_WAKE_ON_RECV); |
| return 0; |
| } |
| |
| default: |
| return QEMUD_PIPE_ERROR_CONNRESET; |
| } |
| } |
| |
| |
| |
| QemudClient* |
| pipeClient_connect( void* opaque, QemudService* svc, int channel ) |
| { |
| PipeClient* pcl; |
| |
| ANEW0(pcl); |
| pcl->pipeSvc = opaque; |
| pcl->client = qemud_client_new( svc, channel, pcl, |
| pipeClient_recv, |
| pipeClient_close, |
| NULL, /* TODO: NO SNAPSHOT SAVE */ |
| NULL ); /* TODO: NO SNAPHOT LOAD */ |
| |
| /* Only for the initial connection message */ |
| qemud_client_set_framing(pcl->client, 1); |
| return pcl->client; |
| } |
| |
| /********************************************************************** |
| ********************************************************************** |
| ***** |
| ***** GLOBAL PIPE STATE |
| ***** |
| *****/ |
| |
| struct PipeService { |
| AIntMap* threadMap; /* maps tid to ThreadState */ |
| }; |
| |
| #if 0 |
| static void |
| pipeService_done(PipeService* pipeSvc) |
| { |
| /* Get rid of the tid -> ThreadState map */ |
| AINTMAP_FOREACH_VALUE(pipeSvc->threadMap, ts, threadState_free(ts)); |
| aintMap_free(pipeSvc->threadMap); |
| pipeSvc->threadMap = NULL; |
| } |
| #endif |
| |
| static void |
| pipeService_init(PipeService* pipeSvc) |
| { |
| pipeSvc->threadMap = aintMap_new(); |
| |
| qemud_service_register( "fast-pipe", 0, pipeSvc, |
| pipeClient_connect, |
| NULL, /* TODO: NO SNAPSHOT SAVE SUPPORT */ |
| NULL /* TODO: NO SNAPSHOT LOAD SUPPORT */ ); |
| } |
| |
| static ThreadState* |
| pipeService_getState(PipeService* pipeSvc, int tid) |
| { |
| if (pipeSvc->threadMap == NULL) |
| pipeSvc->threadMap = aintMap_new(); |
| |
| return (ThreadState*) aintMap_get(pipeSvc->threadMap, tid); |
| } |
| |
| static void |
| pipeService_removeState(PipeService* pipeSvc, int tid) |
| { |
| ThreadState* ts = pipeService_getState(pipeSvc, tid); |
| |
| if (ts == NULL) |
| return; |
| |
| aintMap_del(pipeSvc->threadMap,tid); |
| threadState_free(ts); |
| } |
| |
| static int |
| pipeService_addPipe(PipeService* pipeSvc, int tid, int localId, PipeClient* pcl) |
| { |
| ThreadState* ts = pipeService_getState(pipeSvc, tid); |
| |
| if (ts == NULL) { |
| ts = threadState_new(); |
| aintMap_set(pipeSvc->threadMap, tid, ts); |
| } |
| |
| return threadState_addPipe(ts, localId, pcl); |
| } |
| |
| static void |
| pipeService_removePipe(PipeService* pipeSvc, int tid, int localId) |
| { |
| ThreadState* ts = pipeService_getState(pipeSvc, tid); |
| |
| if (ts == NULL) |
| return; |
| |
| threadState_delPipe(ts, localId); |
| } |
| |
| /********************************************************************** |
| ********************************************************************** |
| ***** |
| ***** HARDWARE API - AS SEEN FROM hw/goldfish_trace.c |
| ***** |
| *****/ |
| |
| static PipeService _globalState[1]; |
| |
| void init_qemud_pipes(void) |
| { |
| pipeService_init(_globalState); |
| } |
| |
| void |
| goldfish_pipe_thread_death(int tid) |
| { |
| PipeService* pipeSvc = _globalState; |
| pipeService_removeState(pipeSvc, tid); |
| } |
| |
| void |
| goldfish_pipe_write(int tid, int offset, uint32_t value) |
| { |
| PipeService* pipeSvc = _globalState; |
| DD("%s: tid=%d offset=%d value=%d (0x%x)", __FUNCTION__, tid, |
| offset, value, value); |
| |
| ThreadState* ts = pipeService_getState(pipeSvc, tid); |
| |
| if (ts == NULL) { |
| D("%s: no thread state for tid=%d", __FUNCTION__, tid); |
| return; |
| } |
| threadState_write(ts, offset, value); |
| } |
| |
| uint32_t |
| goldfish_pipe_read(int tid, int offset) |
| { |
| PipeService* pipeSvc = _globalState; |
| uint32_t ret; |
| |
| DD("%s: tid=%d offset=%d", __FUNCTION__, tid, offset); |
| |
| ThreadState* ts = pipeService_getState(pipeSvc, tid); |
| |
| if (ts == NULL) { |
| D("%s: no thread state for tid=%d", __FUNCTION__, tid); |
| return 0; |
| } |
| ret = threadState_read(ts, offset); |
| DD("%s: result=%d (0x%d)", __FUNCTION__, ret, ret); |
| return ret; |
| } |