| /* |
| * Copyright (C) 2008 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /* |
| * Miscellaneous utility functions. |
| */ |
| #include "Dalvik.h" |
| |
| #include <stdlib.h> |
| #include <stddef.h> |
| #include <string.h> |
| #include <strings.h> |
| #include <ctype.h> |
| #include <time.h> |
| #include <sys/time.h> |
| #include <fcntl.h> |
| #include <cutils/ashmem.h> |
| #include <sys/mman.h> |
| |
| /* |
| * Print a hex dump in this format: |
| * |
| 01234567: 00 11 22 33 44 55 66 77 88 99 aa bb cc dd ee ff 0123456789abcdef\n |
| * |
| * If "mode" is kHexDumpLocal, we start at offset zero, and show a full |
| * 16 bytes on the first line. If it's kHexDumpMem, we make this look |
| * like a memory dump, using the actual address, outputting a partial line |
| * if "vaddr" isn't aligned on a 16-byte boundary. |
| * |
| * "priority" and "tag" determine the values passed to the log calls. |
| * |
| * Does not use printf() or other string-formatting calls. |
| */ |
| void dvmPrintHexDumpEx(int priority, const char* tag, const void* vaddr, |
| size_t length, HexDumpMode mode) |
| { |
| static const char gHexDigit[] = "0123456789abcdef"; |
| const unsigned char* addr = (const unsigned char*)vaddr; |
| char out[77]; /* exact fit */ |
| unsigned int offset; /* offset to show while printing */ |
| char* hex; |
| char* asc; |
| int gap; |
| //int trickle = 0; |
| |
| if (mode == kHexDumpLocal) |
| offset = 0; |
| else |
| offset = (int) addr; |
| |
| memset(out, ' ', sizeof(out)-1); |
| out[8] = ':'; |
| out[sizeof(out)-2] = '\n'; |
| out[sizeof(out)-1] = '\0'; |
| |
| gap = (int) offset & 0x0f; |
| while (length) { |
| unsigned int lineOffset = offset & ~0x0f; |
| int i, count; |
| |
| hex = out; |
| asc = out + 59; |
| |
| for (i = 0; i < 8; i++) { |
| *hex++ = gHexDigit[lineOffset >> 28]; |
| lineOffset <<= 4; |
| } |
| hex++; |
| hex++; |
| |
| count = ((int)length > 16-gap) ? 16-gap : (int)length; /* cap length */ |
| assert(count != 0); |
| assert(count+gap <= 16); |
| |
| if (gap) { |
| /* only on first line */ |
| hex += gap * 3; |
| asc += gap; |
| } |
| |
| for (i = gap ; i < count+gap; i++) { |
| *hex++ = gHexDigit[*addr >> 4]; |
| *hex++ = gHexDigit[*addr & 0x0f]; |
| hex++; |
| if (*addr >= 0x20 && *addr < 0x7f /*isprint(*addr)*/) |
| *asc++ = *addr; |
| else |
| *asc++ = '.'; |
| addr++; |
| } |
| for ( ; i < 16; i++) { |
| /* erase extra stuff; only happens on last line */ |
| *hex++ = ' '; |
| *hex++ = ' '; |
| hex++; |
| *asc++ = ' '; |
| } |
| |
| LOG_PRI(priority, tag, "%s", out); |
| #if 0 //def HAVE_ANDROID_OS |
| /* |
| * We can overrun logcat easily by writing at full speed. On the |
| * other hand, we can make Eclipse time out if we're showing |
| * packet dumps while debugging JDWP. |
| */ |
| { |
| if (trickle++ == 8) { |
| trickle = 0; |
| usleep(20000); |
| } |
| } |
| #endif |
| |
| gap = 0; |
| length -= count; |
| offset += count; |
| } |
| } |
| |
| |
| /* |
| * Fill out a DebugOutputTarget, suitable for printing to the log. |
| */ |
| void dvmCreateLogOutputTarget(DebugOutputTarget* target, int priority, |
| const char* tag) |
| { |
| assert(target != NULL); |
| assert(tag != NULL); |
| |
| target->which = kDebugTargetLog; |
| target->data.log.priority = priority; |
| target->data.log.tag = tag; |
| } |
| |
| /* |
| * Fill out a DebugOutputTarget suitable for printing to a file pointer. |
| */ |
| void dvmCreateFileOutputTarget(DebugOutputTarget* target, FILE* fp) |
| { |
| assert(target != NULL); |
| assert(fp != NULL); |
| |
| target->which = kDebugTargetFile; |
| target->data.file.fp = fp; |
| } |
| |
| /* |
| * Free "target" and any associated data. |
| */ |
| void dvmFreeOutputTarget(DebugOutputTarget* target) |
| { |
| free(target); |
| } |
| |
| /* |
| * Print a debug message, to either a file or the log. |
| */ |
| void dvmPrintDebugMessage(const DebugOutputTarget* target, const char* format, |
| ...) |
| { |
| va_list args; |
| |
| va_start(args, format); |
| |
| switch (target->which) { |
| case kDebugTargetLog: |
| LOG_PRI_VA(target->data.log.priority, target->data.log.tag, |
| format, args); |
| break; |
| case kDebugTargetFile: |
| vfprintf(target->data.file.fp, format, args); |
| break; |
| default: |
| LOGE("unexpected 'which' %d", target->which); |
| break; |
| } |
| |
| va_end(args); |
| } |
| |
| |
| /* |
| * Return a newly-allocated string in which all occurrences of '.' have |
| * been changed to '/'. If we find a '/' in the original string, NULL |
| * is returned to avoid ambiguity. |
| */ |
| char* dvmDotToSlash(const char* str) |
| { |
| char* newStr = strdup(str); |
| char* cp = newStr; |
| |
| if (newStr == NULL) |
| return NULL; |
| |
| while (*cp != '\0') { |
| if (*cp == '/') { |
| assert(false); |
| return NULL; |
| } |
| if (*cp == '.') |
| *cp = '/'; |
| cp++; |
| } |
| |
| return newStr; |
| } |
| |
| std::string dvmHumanReadableDescriptor(const char* descriptor) { |
| // Count the number of '['s to get the dimensionality. |
| const char* c = descriptor; |
| size_t dim = 0; |
| while (*c == '[') { |
| dim++; |
| c++; |
| } |
| |
| // Reference or primitive? |
| if (*c == 'L') { |
| // "[[La/b/C;" -> "a.b.C[][]". |
| c++; // Skip the 'L'. |
| } else { |
| // "[[B" -> "byte[][]". |
| // To make life easier, we make primitives look like unqualified |
| // reference types. |
| switch (*c) { |
| case 'B': c = "byte;"; break; |
| case 'C': c = "char;"; break; |
| case 'D': c = "double;"; break; |
| case 'F': c = "float;"; break; |
| case 'I': c = "int;"; break; |
| case 'J': c = "long;"; break; |
| case 'S': c = "short;"; break; |
| case 'Z': c = "boolean;"; break; |
| default: return descriptor; |
| } |
| } |
| |
| // At this point, 'c' is a string of the form "fully/qualified/Type;" |
| // or "primitive;". Rewrite the type with '.' instead of '/': |
| std::string result; |
| const char* p = c; |
| while (*p != ';') { |
| char ch = *p++; |
| if (ch == '/') { |
| ch = '.'; |
| } |
| result.push_back(ch); |
| } |
| // ...and replace the semicolon with 'dim' "[]" pairs: |
| while (dim--) { |
| result += "[]"; |
| } |
| return result; |
| } |
| |
| std::string dvmHumanReadableType(const Object* obj) |
| { |
| if (obj == NULL) { |
| return "(null)"; |
| } |
| if (obj->clazz == NULL) { |
| /* should only be possible right after a plain dvmMalloc() */ |
| return "(raw)"; |
| } |
| std::string result(dvmHumanReadableDescriptor(obj->clazz->descriptor)); |
| if (dvmIsClassObject(obj)) { |
| const ClassObject* clazz = reinterpret_cast<const ClassObject*>(obj); |
| result += "<" + dvmHumanReadableDescriptor(clazz->descriptor) + ">"; |
| } |
| return result; |
| } |
| |
| /* |
| * Return a newly-allocated string for the "dot version" of the class |
| * name for the given type descriptor. That is, The initial "L" and |
| * final ";" (if any) have been removed and all occurrences of '/' |
| * have been changed to '.'. |
| * |
| * "Dot version" names are used in the class loading machinery. |
| * See also dvmHumanReadableDescriptor. |
| */ |
| char* dvmDescriptorToDot(const char* str) |
| { |
| size_t at = strlen(str); |
| char* newStr; |
| |
| if ((at >= 2) && (str[0] == 'L') && (str[at - 1] == ';')) { |
| at -= 2; /* Two fewer chars to copy. */ |
| str++; /* Skip the 'L'. */ |
| } |
| |
| newStr = (char*)malloc(at + 1); /* Add one for the '\0'. */ |
| if (newStr == NULL) |
| return NULL; |
| |
| newStr[at] = '\0'; |
| |
| while (at > 0) { |
| at--; |
| newStr[at] = (str[at] == '/') ? '.' : str[at]; |
| } |
| |
| return newStr; |
| } |
| |
| /* |
| * Return a newly-allocated string for the type descriptor |
| * corresponding to the "dot version" of the given class name. That |
| * is, non-array names are surrounded by "L" and ";", and all |
| * occurrences of '.' have been changed to '/'. |
| * |
| * "Dot version" names are used in the class loading machinery. |
| */ |
| char* dvmDotToDescriptor(const char* str) |
| { |
| size_t length = strlen(str); |
| int wrapElSemi = 0; |
| char* newStr; |
| char* at; |
| |
| if (str[0] != '[') { |
| length += 2; /* for "L" and ";" */ |
| wrapElSemi = 1; |
| } |
| |
| newStr = at = (char*)malloc(length + 1); /* + 1 for the '\0' */ |
| |
| if (newStr == NULL) { |
| return NULL; |
| } |
| |
| if (wrapElSemi) { |
| *(at++) = 'L'; |
| } |
| |
| while (*str) { |
| char c = *(str++); |
| if (c == '.') { |
| c = '/'; |
| } |
| *(at++) = c; |
| } |
| |
| if (wrapElSemi) { |
| *(at++) = ';'; |
| } |
| |
| *at = '\0'; |
| return newStr; |
| } |
| |
| /* |
| * Return a newly-allocated string for the internal-form class name for |
| * the given type descriptor. That is, the initial "L" and final ";" (if |
| * any) have been removed. |
| */ |
| char* dvmDescriptorToName(const char* str) |
| { |
| if (str[0] == 'L') { |
| size_t length = strlen(str) - 1; |
| char* newStr = (char*)malloc(length); |
| |
| if (newStr == NULL) { |
| return NULL; |
| } |
| |
| strlcpy(newStr, str + 1, length); |
| return newStr; |
| } |
| |
| return strdup(str); |
| } |
| |
| /* |
| * Return a newly-allocated string for the type descriptor for the given |
| * internal-form class name. That is, a non-array class name will get |
| * surrounded by "L" and ";", while array names are left as-is. |
| */ |
| char* dvmNameToDescriptor(const char* str) |
| { |
| if (str[0] != '[') { |
| size_t length = strlen(str); |
| char* descriptor = (char*)malloc(length + 3); |
| |
| if (descriptor == NULL) { |
| return NULL; |
| } |
| |
| descriptor[0] = 'L'; |
| strcpy(descriptor + 1, str); |
| descriptor[length + 1] = ';'; |
| descriptor[length + 2] = '\0'; |
| |
| return descriptor; |
| } |
| |
| return strdup(str); |
| } |
| |
| /* |
| * Get a notion of the current time, in nanoseconds. This is meant for |
| * computing durations (e.g. "operation X took 52nsec"), so the result |
| * should not be used to get the current date/time. |
| */ |
| u8 dvmGetRelativeTimeNsec() |
| { |
| #ifdef HAVE_POSIX_CLOCKS |
| struct timespec now; |
| clock_gettime(CLOCK_MONOTONIC, &now); |
| return (u8)now.tv_sec*1000000000LL + now.tv_nsec; |
| #else |
| struct timeval now; |
| gettimeofday(&now, NULL); |
| return (u8)now.tv_sec*1000000000LL + now.tv_usec * 1000LL; |
| #endif |
| } |
| |
| /* |
| * Get the per-thread CPU time, in nanoseconds. |
| * |
| * Only useful for time deltas. |
| */ |
| u8 dvmGetThreadCpuTimeNsec() |
| { |
| #ifdef HAVE_POSIX_CLOCKS |
| struct timespec now; |
| clock_gettime(CLOCK_THREAD_CPUTIME_ID, &now); |
| return (u8)now.tv_sec*1000000000LL + now.tv_nsec; |
| #else |
| return (u8) -1; |
| #endif |
| } |
| |
| /* |
| * Get the per-thread CPU time, in nanoseconds, for the specified thread. |
| */ |
| u8 dvmGetOtherThreadCpuTimeNsec(pthread_t thread) |
| { |
| #if 0 /*def HAVE_POSIX_CLOCKS*/ |
| int clockId; |
| |
| if (pthread_getcpuclockid(thread, &clockId) != 0) |
| return (u8) -1; |
| |
| struct timespec now; |
| clock_gettime(clockId, &now); |
| return (u8)now.tv_sec*1000000000LL + now.tv_nsec; |
| #else |
| return (u8) -1; |
| #endif |
| } |
| |
| |
| /* |
| * Call this repeatedly, with successively higher values for "iteration", |
| * to sleep for a period of time not to exceed "maxTotalSleep". |
| * |
| * For example, when called with iteration==0 we will sleep for a very |
| * brief time. On the next call we will sleep for a longer time. When |
| * the sum total of all sleeps reaches "maxTotalSleep", this returns false. |
| * |
| * The initial start time value for "relStartTime" MUST come from the |
| * dvmGetRelativeTimeUsec call. On the device this must come from the |
| * monotonic clock source, not the wall clock. |
| * |
| * This should be used wherever you might be tempted to call sched_yield() |
| * in a loop. The problem with sched_yield is that, for a high-priority |
| * thread, the kernel might not actually transfer control elsewhere. |
| * |
| * Returns "false" if we were unable to sleep because our time was up. |
| */ |
| bool dvmIterativeSleep(int iteration, int maxTotalSleep, u8 relStartTime) |
| { |
| const int minSleep = 10000; |
| u8 curTime; |
| int curDelay; |
| |
| /* |
| * Get current time, and see if we've already exceeded the limit. |
| */ |
| curTime = dvmGetRelativeTimeUsec(); |
| if (curTime >= relStartTime + maxTotalSleep) { |
| LOGVV("exsl: sleep exceeded (start=%llu max=%d now=%llu)", |
| relStartTime, maxTotalSleep, curTime); |
| return false; |
| } |
| |
| /* |
| * Compute current delay. We're bounded by "maxTotalSleep", so no |
| * real risk of overflow assuming "usleep" isn't returning early. |
| * (Besides, 2^30 usec is about 18 minutes by itself.) |
| * |
| * For iteration==0 we just call sched_yield(), so the first sleep |
| * at iteration==1 is actually (minSleep * 2). |
| */ |
| curDelay = minSleep; |
| while (iteration-- > 0) |
| curDelay *= 2; |
| assert(curDelay > 0); |
| |
| if (curTime + curDelay >= relStartTime + maxTotalSleep) { |
| LOGVV("exsl: reduced delay from %d to %d", |
| curDelay, (int) ((relStartTime + maxTotalSleep) - curTime)); |
| curDelay = (int) ((relStartTime + maxTotalSleep) - curTime); |
| } |
| |
| if (iteration == 0) { |
| LOGVV("exsl: yield"); |
| sched_yield(); |
| } else { |
| LOGVV("exsl: sleep for %d", curDelay); |
| usleep(curDelay); |
| } |
| return true; |
| } |
| |
| |
| /* |
| * Set the "close on exec" flag so we don't expose our file descriptors |
| * to processes launched by us. |
| */ |
| bool dvmSetCloseOnExec(int fd) |
| { |
| int flags; |
| |
| /* |
| * There's presently only one flag defined, so getting the previous |
| * value of the fd flags is probably unnecessary. |
| */ |
| flags = fcntl(fd, F_GETFD); |
| if (flags < 0) { |
| LOGW("Unable to get fd flags for fd %d", fd); |
| return false; |
| } |
| if (fcntl(fd, F_SETFD, flags | FD_CLOEXEC) < 0) { |
| LOGW("Unable to set close-on-exec for fd %d", fd); |
| return false; |
| } |
| return true; |
| } |
| |
| #if (!HAVE_STRLCPY) |
| /* Implementation of strlcpy() for platforms that don't already have it. */ |
| size_t strlcpy(char *dst, const char *src, size_t size) { |
| size_t srcLength = strlen(src); |
| size_t copyLength = srcLength; |
| |
| if (srcLength > (size - 1)) { |
| copyLength = size - 1; |
| } |
| |
| if (size != 0) { |
| strncpy(dst, src, copyLength); |
| dst[copyLength] = '\0'; |
| } |
| |
| return srcLength; |
| } |
| #endif |
| |
| /* |
| * Allocates a memory region using ashmem and mmap, initialized to |
| * zero. Actual allocation rounded up to page multiple. Returns |
| * NULL on failure. |
| */ |
| void *dvmAllocRegion(size_t byteCount, int prot, const char *name) { |
| void *base; |
| int fd, ret; |
| |
| byteCount = ALIGN_UP_TO_PAGE_SIZE(byteCount); |
| fd = ashmem_create_region(name, byteCount); |
| if (fd == -1) { |
| return NULL; |
| } |
| base = mmap(NULL, byteCount, prot, MAP_PRIVATE, fd, 0); |
| ret = close(fd); |
| if (base == MAP_FAILED) { |
| return NULL; |
| } |
| if (ret == -1) { |
| return NULL; |
| } |
| return base; |
| } |
| |
| /* |
| * Get some per-thread stats. |
| * |
| * This is currently generated by opening the appropriate "stat" file |
| * in /proc and reading the pile of stuff that comes out. |
| */ |
| bool dvmGetThreadStats(ProcStatData* pData, pid_t tid) |
| { |
| /* |
| int pid; |
| char comm[128]; |
| char state; |
| int ppid, pgrp, session, tty_nr, tpgid; |
| unsigned long flags, minflt, cminflt, majflt, cmajflt, utime, stime; |
| long cutime, cstime, priority, nice, zero, itrealvalue; |
| unsigned long starttime, vsize; |
| long rss; |
| unsigned long rlim, startcode, endcode, startstack, kstkesp, kstkeip; |
| unsigned long signal, blocked, sigignore, sigcatch, wchan, nswap, cnswap; |
| int exit_signal, processor; |
| unsigned long rt_priority, policy; |
| |
| scanf("%d %s %c %d %d %d %d %d %lu %lu %lu %lu %lu %lu %lu %ld %ld %ld " |
| "%ld %ld %ld %lu %lu %ld %lu %lu %lu %lu %lu %lu %lu %lu %lu %lu " |
| "%lu %lu %lu %d %d %lu %lu", |
| &pid, comm, &state, &ppid, &pgrp, &session, &tty_nr, &tpgid, |
| &flags, &minflt, &cminflt, &majflt, &cmajflt, &utime, &stime, |
| &cutime, &cstime, &priority, &nice, &zero, &itrealvalue, |
| &starttime, &vsize, &rss, &rlim, &startcode, &endcode, |
| &startstack, &kstkesp, &kstkeip, &signal, &blocked, &sigignore, |
| &sigcatch, &wchan, &nswap, &cnswap, &exit_signal, &processor, |
| &rt_priority, &policy); |
| |
| (new: delayacct_blkio_ticks %llu (since Linux 2.6.18)) |
| */ |
| |
| char nameBuf[64]; |
| int i, fd; |
| |
| /* |
| * Open and read the appropriate file. This is expected to work on |
| * Linux but will fail on other platforms (e.g. Mac sim). |
| */ |
| sprintf(nameBuf, "/proc/self/task/%d/stat", (int) tid); |
| fd = open(nameBuf, O_RDONLY); |
| if (fd < 0) { |
| LOGV("Unable to open '%s': %s", nameBuf, strerror(errno)); |
| return false; |
| } |
| |
| char lineBuf[512]; /* > 2x typical */ |
| int cc = read(fd, lineBuf, sizeof(lineBuf)-1); |
| if (cc <= 0) { |
| const char* msg = (cc == 0) ? "unexpected EOF" : strerror(errno); |
| LOGI("Unable to read '%s': %s", nameBuf, msg); |
| close(fd); |
| return false; |
| } |
| close(fd); |
| lineBuf[cc] = '\0'; |
| |
| /* |
| * Skip whitespace-separated tokens. For the most part we can assume |
| * that tokens do not contain spaces, and are separated by exactly one |
| * space character. The only exception is the second field ("comm") |
| * which may contain spaces but is surrounded by parenthesis. |
| */ |
| char* cp = strchr(lineBuf, ')'); |
| if (cp == NULL) |
| goto parse_fail; |
| cp++; |
| for (i = 2; i < 13; i++) { |
| cp = strchr(cp+1, ' '); |
| if (cp == NULL) |
| goto parse_fail; |
| } |
| |
| /* |
| * Grab utime/stime. |
| */ |
| char* endp; |
| pData->utime = strtoul(cp+1, &endp, 10); |
| if (endp == cp+1) |
| LOGI("Warning: strtoul failed on utime ('%.30s...')", cp); |
| |
| cp = strchr(cp+1, ' '); |
| if (cp == NULL) |
| goto parse_fail; |
| |
| pData->stime = strtoul(cp+1, &endp, 10); |
| if (endp == cp+1) |
| LOGI("Warning: strtoul failed on stime ('%.30s...')", cp); |
| |
| /* |
| * Skip more stuff we don't care about. |
| */ |
| for (i = 14; i < 38; i++) { |
| cp = strchr(cp+1, ' '); |
| if (cp == NULL) |
| goto parse_fail; |
| } |
| |
| /* |
| * Grab processor number. |
| */ |
| pData->processor = strtol(cp+1, &endp, 10); |
| if (endp == cp+1) |
| LOGI("Warning: strtoul failed on processor ('%.30s...')", cp); |
| |
| return true; |
| |
| parse_fail: |
| LOGI("stat parse failed (%s)", lineBuf); |
| return false; |
| } |
| |
| /* documented in header file */ |
| const char* dvmPathToAbsolutePortion(const char* path) { |
| if (path == NULL) { |
| return NULL; |
| } |
| |
| if (path[0] == '/') { |
| /* It's a regular absolute path. Return it. */ |
| return path; |
| } |
| |
| const char* sentinel = strstr(path, "/./"); |
| |
| if (sentinel != NULL) { |
| /* It's got the sentinel. Return a pointer to the second slash. */ |
| return sentinel + 2; |
| } |
| |
| return NULL; |
| } |
| |
| // From RE2. |
| static void StringAppendV(std::string* dst, const char* format, va_list ap) { |
| // First try with a small fixed size buffer |
| char space[1024]; |
| |
| // It's possible for methods that use a va_list to invalidate |
| // the data in it upon use. The fix is to make a copy |
| // of the structure before using it and use that copy instead. |
| va_list backup_ap; |
| va_copy(backup_ap, ap); |
| int result = vsnprintf(space, sizeof(space), format, backup_ap); |
| va_end(backup_ap); |
| |
| if ((result >= 0) && ((size_t) result < sizeof(space))) { |
| // It fit |
| dst->append(space, result); |
| return; |
| } |
| |
| // Repeatedly increase buffer size until it fits |
| int length = sizeof(space); |
| while (true) { |
| if (result < 0) { |
| // Older behavior: just try doubling the buffer size |
| length *= 2; |
| } else { |
| // We need exactly "result+1" characters |
| length = result+1; |
| } |
| char* buf = new char[length]; |
| |
| // Restore the va_list before we use it again |
| va_copy(backup_ap, ap); |
| result = vsnprintf(buf, length, format, backup_ap); |
| va_end(backup_ap); |
| |
| if ((result >= 0) && (result < length)) { |
| // It fit |
| dst->append(buf, result); |
| delete[] buf; |
| return; |
| } |
| delete[] buf; |
| } |
| } |
| |
| std::string dvmStringPrintf(const char* fmt, ...) { |
| va_list ap; |
| va_start(ap, fmt); |
| std::string result; |
| StringAppendV(&result, fmt, ap); |
| va_end(ap); |
| return result; |
| } |