/** Join thread. This function provides combined implementation for pthread_join() and pthread_join_with_handle() functions. @param thread reference to pthread object @param handle thread handle of thread to be joined @return int @retval 0 success @retval 1 failure */ int pthread_join_base(pthread_t thread, HANDLE handle) { DWORD ret; if(!handle) { handle = OpenThread(SYNCHRONIZE, FALSE, thread); if(!handle) { my_osmaperr(GetLastError()); goto error_return; } } ret = WaitForSingleObject(handle, INFINITE); if(ret != WAIT_OBJECT_0) { my_osmaperr(GetLastError()); goto error_return; } CloseHandle(handle); return 0; error_return: if(handle) CloseHandle(handle); return 1; }
/* Quick and dirty my_fstat() implementation for Windows. Use CRT fstat on temporarily allocated file descriptor. Patch file size, because size that fstat returns is not reliable (may be outdated) */ int my_win_fstat(File fd, struct _stati64 *buf) { int crt_fd; int retval; HANDLE hFile, hDup; DBUG_ENTER("my_win_fstat"); hFile= my_get_osfhandle(fd); if(!DuplicateHandle( GetCurrentProcess(), hFile, GetCurrentProcess(), &hDup ,0,FALSE,DUPLICATE_SAME_ACCESS)) { my_osmaperr(GetLastError()); DBUG_RETURN(-1); } if ((crt_fd= _open_osfhandle((intptr_t)hDup,0)) < 0) DBUG_RETURN(-1); retval= _fstati64(crt_fd, buf); if(retval == 0) { /* File size returned by stat is not accurate (may be outdated), fix it*/ GetFileSizeEx(hDup, (PLARGE_INTEGER) (&(buf->st_size))); } _close(crt_fd); DBUG_RETURN(retval); }
size_t my_win_write(File fd, const uchar *Buffer, size_t Count) { DWORD nWritten; OVERLAPPED ov; OVERLAPPED *pov= NULL; HANDLE hFile; DBUG_ENTER("my_win_write"); DBUG_PRINT("my",("Filedes: %d, Buffer: %p, Count %zd", fd, Buffer, Count)); if(my_get_open_flags(fd) & _O_APPEND) { /* Atomic append to the end of file is is done by special initialization of the OVERLAPPED structure. See MSDN WriteFile documentation for more info. */ memset(&ov, 0, sizeof(ov)); ov.Offset= FILE_WRITE_TO_END_OF_FILE; ov.OffsetHigh= -1; pov= &ov; } hFile= my_get_osfhandle(fd); if(!WriteFile(hFile, Buffer, (DWORD)Count, &nWritten, pov)) { nWritten= (size_t)-1; my_osmaperr(GetLastError()); } DBUG_RETURN((size_t)nWritten); }
/** Create thread. This function provides combined implementation for pthread_create() and pthread_create_get_handle() functions. @param thread_id reference to pthread object @param attr reference to pthread attribute @param func pthread handler function @param param parameters to pass to newly created thread @param out_handle reference to thread handle. This needs to be passed to pthread_join_with_handle() function when it is joined @return int @retval 0 success @retval 1 failure */ int pthread_create_base(pthread_t* thread_id, const pthread_attr_t* attr, pthread_handler func, void* param, HANDLE* out_handle) { HANDLE handle= NULL; struct thread_start_parameter* parameter; unsigned int stack_size; parameter = (struct thread_start_parameter*)malloc(sizeof(*parameter)); if(!parameter) goto error_return; parameter->func = func; parameter->arg = param; stack_size = attr?attr->dwStackSize:0; handle= (HANDLE)_beginthreadex(NULL, stack_size, pthread_start, parameter, 0, thread_id); if(!handle) { my_osmaperr(GetLastError()); free(parameter); goto error_return; } if(!out_handle) /* Do not need thread handle, close it */ CloseHandle(handle); else /* Save thread handle, it will be used later during join */ *out_handle= handle; return 0; error_return: return 1; }
size_t my_win_pwrite(File Filedes, const uchar *Buffer, size_t Count, my_off_t offset) { DWORD nBytesWritten; HANDLE hFile; OVERLAPPED ov= {0}; LARGE_INTEGER li; DBUG_ENTER("my_win_pwrite"); DBUG_PRINT("my",("Filedes: %d, Buffer: %p, Count: %llu, offset: %llu", Filedes, Buffer, (ulonglong)Count, (ulonglong)offset)); if(!Count) DBUG_RETURN(0); #ifdef _WIN64 if(Count > UINT_MAX) Count= UINT_MAX; #endif hFile= (HANDLE)my_get_osfhandle(Filedes); li.QuadPart= offset; ov.Offset= li.LowPart; ov.OffsetHigh= li.HighPart; if(!WriteFile(hFile, Buffer, (DWORD)Count, &nBytesWritten, &ov)) { my_osmaperr(GetLastError()); DBUG_RETURN((size_t)-1); } else DBUG_RETURN(nBytesWritten); }
size_t my_win_read(File Filedes, uchar *Buffer, size_t Count) { DWORD nBytesRead; HANDLE hFile; DBUG_ENTER("my_win_read"); if(!Count) DBUG_RETURN(0); #ifdef _WIN64 if(Count > UINT_MAX) Count= UINT_MAX; #endif hFile= (HANDLE)my_get_osfhandle(Filedes); if(!ReadFile(hFile, Buffer, (DWORD)Count, &nBytesRead, NULL)) { DWORD lastError= GetLastError(); /* ERROR_BROKEN_PIPE is returned when no more data coming through e.g. a command pipe in windows : see MSDN on ReadFile. */ if(lastError == ERROR_HANDLE_EOF || lastError == ERROR_BROKEN_PIPE) DBUG_RETURN(0); /*return 0 at EOF*/ my_osmaperr(lastError); DBUG_RETURN((size_t)-1); } DBUG_RETURN(nBytesRead); }
size_t my_win_pread(File Filedes, uchar *Buffer, size_t Count, my_off_t offset) { DWORD nBytesRead; HANDLE hFile; OVERLAPPED ov= {0}; LARGE_INTEGER li; DBUG_ENTER("my_win_pread"); if(!Count) DBUG_RETURN(0); #ifdef _WIN64 if(Count > UINT_MAX) Count= UINT_MAX; #endif hFile= (HANDLE)my_get_osfhandle(Filedes); li.QuadPart= offset; ov.Offset= li.LowPart; ov.OffsetHigh= li.HighPart; if(!ReadFile(hFile, Buffer, (DWORD)Count, &nBytesRead, &ov)) { DWORD lastError= GetLastError(); /* ERROR_BROKEN_PIPE is returned when no more data coming through e.g. a command pipe in windows : see MSDN on ReadFile. */ if(lastError == ERROR_HANDLE_EOF || lastError == ERROR_BROKEN_PIPE) DBUG_RETURN(0); /*return 0 at EOF*/ my_osmaperr(lastError); DBUG_RETURN((size_t)-1); } DBUG_RETURN(nBytesRead); }
int my_thread_create(my_thread_handle *thread, const my_thread_attr_t *attr, my_start_routine func, void *arg) { #ifndef _WIN32 return pthread_create(&thread->thread, attr, func, arg); #else struct thread_start_parameter *par; unsigned int stack_size; par= (struct thread_start_parameter *)malloc(sizeof(*par)); if (!par) goto error_return; par->func= func; par->arg= arg; stack_size= attr ? attr->dwStackSize : 0; thread->handle= (HANDLE)_beginthreadex(NULL, stack_size, win_thread_start, par, 0, &thread->thread); if (thread->handle) return 0; my_osmaperr(GetLastError()); free(par); error_return: thread->thread= 0; thread->handle= NULL; return 1; #endif }
int my_rename(const char *from, const char *to, myf MyFlags) { int error = 0; DBUG_ENTER("my_rename"); DBUG_PRINT("my",("from %s to %s MyFlags %d", from, to, MyFlags)); #if defined(HAVE_FILE_VERSIONS) { /* Check that there isn't a old file */ int save_errno; MY_STAT my_stat_result; save_errno=my_errno; if (my_stat(to,&my_stat_result,MYF(0))) { my_errno=EEXIST; error= -1; if (MyFlags & MY_FAE+MY_WME) { char errbuf[MYSYS_STRERROR_SIZE]; my_error(EE_LINK, MYF(ME_BELL+ME_WAITTANG), from, to, my_errno, my_strerror(errbuf, sizeof(errbuf), my_errno)); } DBUG_RETURN(error); } my_errno=save_errno; } #endif #if defined(__WIN__) if(!MoveFileEx(from, to, MOVEFILE_COPY_ALLOWED| MOVEFILE_REPLACE_EXISTING)) { my_osmaperr(GetLastError()); #else if (rename(from,to)) { #endif my_errno=errno; error = -1; if (MyFlags & (MY_FAE+MY_WME)) { char errbuf[MYSYS_STRERROR_SIZE]; my_error(EE_LINK, MYF(ME_BELL+ME_WAITTANG), from, to, my_errno, my_strerror(errbuf, sizeof(errbuf), my_errno)); } } else if (MyFlags & MY_SYNC_DIR) { #ifdef NEED_EXPLICIT_SYNC_DIR /* do only the needed amount of syncs: */ char dir_from[FN_REFLEN], dir_to[FN_REFLEN]; size_t dir_from_length, dir_to_length; dirname_part(dir_from, from, &dir_from_length); dirname_part(dir_to, to, &dir_to_length); if (my_sync_dir(dir_from, MyFlags) || (strcmp(dir_from, dir_to) && my_sync_dir(dir_to, MyFlags))) error= -1; #endif } DBUG_RETURN(error); } /* my_rename */
size_t my_win_pread(File Filedes, uchar *Buffer, size_t Count, my_off_t offset) { DWORD nBytesRead; HANDLE hFile; OVERLAPPED ov= {0}; LARGE_INTEGER li; DBUG_ENTER("my_win_pread"); if(!Count) DBUG_RETURN(0); #ifdef _WIN64 if(Count > UINT_MAX) Count= UINT_MAX; #endif hFile= (HANDLE)my_get_osfhandle(Filedes); li.QuadPart= offset; ov.Offset= li.LowPart; ov.OffsetHigh= li.HighPart; if(!ReadFile(hFile, Buffer, (DWORD)Count, &nBytesRead, &ov)) { DWORD lastError= GetLastError(); if(lastError == ERROR_HANDLE_EOF) DBUG_RETURN(0); /*return 0 at EOF*/ my_osmaperr(lastError); DBUG_RETURN(-1); } DBUG_RETURN(nBytesRead); }
int my_win_fsync(File fd) { DBUG_ENTER("my_win_fsync"); if(FlushFileBuffers(my_get_osfhandle(fd))) DBUG_RETURN(0); my_osmaperr(GetLastError()); DBUG_RETURN(-1); }
int my_win_close(File fd) { DBUG_ENTER("my_win_close"); if(CloseHandle(my_get_osfhandle(fd))) { invalidate_fd(fd); DBUG_RETURN(0); } my_osmaperr(GetLastError()); DBUG_RETURN(-1); }
int my_win_dup(File fd) { HANDLE hDup; DBUG_ENTER("my_win_dup"); if (DuplicateHandle(GetCurrentProcess(), my_get_osfhandle(fd), GetCurrentProcess(), &hDup, 0, FALSE, DUPLICATE_SAME_ACCESS)) { DBUG_RETURN(my_open_osfhandle(hDup, my_get_open_flags(fd))); } my_osmaperr(GetLastError()); DBUG_RETURN(-1); }
int my_thread_create(my_thread_handle *thread, const my_thread_attr_t *attr, my_start_routine func, void *arg) { #ifndef _WIN32 return pthread_create(&thread->thread, attr, func, arg); #else struct thread_start_parameter *par; unsigned int stack_size; par= (struct thread_start_parameter *)malloc(sizeof(*par)); if (!par) goto error_return; par->func= func; par->arg= arg; stack_size= attr ? attr->dwStackSize : 0; thread->handle= (HANDLE)_beginthreadex(NULL, stack_size, win_thread_start, par, 0, &thread->thread); if (thread->handle) { /* Note that JOINABLE is default, so attr == NULL => JOINABLE. */ if (attr && attr->detachstate == MY_THREAD_CREATE_DETACHED) { /* Close handles for detached threads right away to avoid leaking handles. For joinable threads we need the handle during my_thread_join. It will be closed there. */ CloseHandle(thread->handle); thread->handle= NULL; } return 0; } my_osmaperr(GetLastError()); free(par); error_return: thread->thread= 0; thread->handle= NULL; return 1; #endif }
int my_win_chsize(File fd, my_off_t newlength) { HANDLE hFile; LARGE_INTEGER length; DBUG_ENTER("my_win_chsize"); hFile= (HANDLE) my_get_osfhandle(fd); length.QuadPart= newlength; if (!SetFilePointerEx(hFile, length , NULL , FILE_BEGIN)) goto err; if (!SetEndOfFile(hFile)) goto err; DBUG_RETURN(0); err: my_osmaperr(GetLastError()); my_errno= errno; DBUG_RETURN(-1); }
my_off_t my_win_lseek(File fd, my_off_t pos, int whence) { LARGE_INTEGER offset; LARGE_INTEGER newpos; DBUG_ENTER("my_win_lseek"); /* Check compatibility of Windows and Posix seek constants */ compile_time_assert(FILE_BEGIN == SEEK_SET && FILE_CURRENT == SEEK_CUR && FILE_END == SEEK_END); offset.QuadPart= pos; if(!SetFilePointerEx(my_get_osfhandle(fd), offset, &newpos, whence)) { my_osmaperr(GetLastError()); newpos.QuadPart= -1; } DBUG_RETURN(newpos.QuadPart); }
int my_thread_join(my_thread_handle *thread, void **value_ptr) { #ifndef _WIN32 return pthread_join(thread->thread, value_ptr); #else DWORD ret; int result= 0; ret= WaitForSingleObject(thread->handle, INFINITE); if (ret != WAIT_OBJECT_0) { my_osmaperr(GetLastError()); result= 1; } if (thread->handle) CloseHandle(thread->handle); thread->thread= 0; thread->handle= NULL; return result; #endif }
static int win_lock(File fd, int locktype, my_off_t start, my_off_t length, int timeout_sec) { LARGE_INTEGER liOffset,liLength; DWORD dwFlags; OVERLAPPED ov= {0}; HANDLE hFile= (HANDLE)my_get_osfhandle(fd); DWORD lastError= 0; int i; int timeout_millis= timeout_sec * 1000; DBUG_ENTER("win_lock"); liOffset.QuadPart= start; liLength.QuadPart= length; ov.Offset= liOffset.LowPart; ov.OffsetHigh= liOffset.HighPart; if (locktype == F_UNLCK) { if (UnlockFileEx(hFile, 0, liLength.LowPart, liLength.HighPart, &ov)) DBUG_RETURN(0); /* For compatibility with fcntl implementation, ignore error, if region was not locked */ if (GetLastError() == ERROR_NOT_LOCKED) { SetLastError(0); DBUG_RETURN(0); } goto error; } else if (locktype == F_RDLCK) /* read lock is mapped to a shared lock. */ dwFlags= 0; else /* write lock is mapped to an exclusive lock. */ dwFlags= LOCKFILE_EXCLUSIVE_LOCK; /* Drop old lock first to avoid double locking. During analyze of Bug#38133 (Myisamlog test fails on Windows) I met the situation that the program myisamlog locked the file exclusively, then additionally shared, then did one unlock, and then blocked on an attempt to lock it exclusively again. Unlocking before every lock fixed the problem. Note that this introduces a race condition. When the application wants to convert an exclusive lock into a shared one, it will now first unlock the file and then lock it shared. A waiting exclusive lock could step in here. For reasons described in Bug#38133 and Bug#41124 (Server hangs on Windows with --external-locking after INSERT...SELECT) and in the review thread at http://lists.mysql.com/commits/60721 it seems to be the better option than not to unlock here. If one day someone notices a way how to do file lock type changes on Windows without unlocking before taking the new lock, please change this code accordingly to fix the race condition. */ if (!UnlockFileEx(hFile, 0, liLength.LowPart, liLength.HighPart, &ov) && (GetLastError() != ERROR_NOT_LOCKED)) goto error; if (timeout_sec == WIN_LOCK_INFINITE) { if (LockFileEx(hFile, dwFlags, 0, liLength.LowPart, liLength.HighPart, &ov)) DBUG_RETURN(0); goto error; } dwFlags|= LOCKFILE_FAIL_IMMEDIATELY; timeout_millis= timeout_sec * 1000; /* Try lock in a loop, until the lock is acquired or timeout happens */ for(i= 0; ;i+= WIN_LOCK_SLEEP_MILLIS) { if (LockFileEx(hFile, dwFlags, 0, liLength.LowPart, liLength.HighPart, &ov)) DBUG_RETURN(0); if (GetLastError() != ERROR_LOCK_VIOLATION) goto error; if (i >= timeout_millis) break; Sleep(WIN_LOCK_SLEEP_MILLIS); } /* timeout */ errno= EAGAIN; DBUG_RETURN(-1); error: my_osmaperr(GetLastError()); DBUG_RETURN(-1); }
/* Delete file. The function also makes best effort to minimize number of errors, where another program (or thread in the current program) has the the same file open. We're using 2 tricks to prevent the errors. 1. A usual Win32's DeleteFile() can with ERROR_SHARED_VIOLATION, because the file is opened in another application (often, antivirus or backup) We avoid the error by using CreateFile() with FILE_FLAG_DELETE_ON_CLOSE, instead of DeleteFile() 2. If file which is deleted (delete on close) but has not entirely gone, because it is still opened by some app, an attempt to trcreate file with the same name would result in yet another error. The workaround here is renaming a file to unique name. Symbolic link are deleted without renaming. Directories are not deleted. */ static int my_win_unlink(const char *name) { HANDLE handle= INVALID_HANDLE_VALUE; DWORD attributes; DWORD last_error; char unique_filename[MAX_PATH + 35]; unsigned long long tsc; /* time stamp counter, for unique filename*/ DBUG_ENTER("my_win_unlink"); attributes= GetFileAttributes(name); if (attributes == INVALID_FILE_ATTRIBUTES) { last_error= GetLastError(); DBUG_PRINT("error",("GetFileAttributes(%s) failed with %u\n", name, last_error)); goto error; } if (attributes & FILE_ATTRIBUTE_DIRECTORY) { DBUG_PRINT("error",("can't remove %s - it is a directory\n", name)); errno= EINVAL; DBUG_RETURN(-1); } if (attributes & FILE_ATTRIBUTE_REPARSE_POINT) { /* Symbolic link. Delete link, the not target */ if (!DeleteFile(name)) { last_error= GetLastError(); DBUG_PRINT("error",("DeleteFile(%s) failed with %u\n", name,last_error)); goto error; } DBUG_RETURN(0); } handle= CreateFile(name, DELETE, 0, NULL, OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, NULL); if (handle != INVALID_HANDLE_VALUE) { /* We opened file without sharing flags (exclusive), no one else has this file opened, thus it is save to close handle to remove it. No renaming is necessary. */ CloseHandle(handle); DBUG_RETURN(0); } /* Can't open file exclusively, hence the file must be already opened by someone else. Open it for delete (with all FILE_SHARE flags set), rename to unique name, close. */ handle= CreateFile(name, DELETE, FILE_SHARE_READ|FILE_SHARE_WRITE|FILE_SHARE_DELETE, NULL, OPEN_EXISTING, FILE_FLAG_DELETE_ON_CLOSE, NULL); if (handle == INVALID_HANDLE_VALUE) { last_error= GetLastError(); DBUG_PRINT("error", ("CreateFile(%s) with FILE_FLAG_DELETE_ON_CLOSE failed with %u\n", name,last_error)); goto error; } tsc= __rdtsc(); my_snprintf(unique_filename,sizeof(unique_filename),"%s.%llx.deleted", name, tsc); if (!MoveFile(name, unique_filename)) { DBUG_PRINT("warning", ("moving %s to unique filename failed, error %u\n", name,GetLastError())); } CloseHandle(handle); DBUG_RETURN(0); error: my_osmaperr(last_error); DBUG_RETURN(-1); }
File my_win_sopen(const char *path, int oflag, int shflag, int pmode) { int fh; /* handle of opened file */ int mask; HANDLE osfh; /* OS handle of opened file */ DWORD fileaccess; /* OS file access (requested) */ DWORD fileshare; /* OS file sharing mode */ DWORD filecreate; /* OS method of opening/creating */ DWORD fileattrib; /* OS file attribute flags */ SECURITY_ATTRIBUTES SecurityAttributes; DBUG_ENTER("my_win_sopen"); if (check_if_legal_filename(path)) { errno= EACCES; DBUG_RETURN(-1); } SecurityAttributes.nLength= sizeof(SecurityAttributes); SecurityAttributes.lpSecurityDescriptor= NULL; SecurityAttributes.bInheritHandle= !(oflag & _O_NOINHERIT); /* decode the access flags */ switch (oflag & (_O_RDONLY | _O_WRONLY | _O_RDWR)) { case _O_RDONLY: /* read access */ fileaccess= GENERIC_READ; break; case _O_WRONLY: /* write access */ fileaccess= GENERIC_WRITE; break; case _O_RDWR: /* read and write access */ fileaccess= GENERIC_READ | GENERIC_WRITE; break; default: /* error, bad oflag */ errno= EINVAL; DBUG_RETURN(-1); } /* decode sharing flags */ switch (shflag) { case _SH_DENYRW: /* exclusive access except delete */ fileshare= FILE_SHARE_DELETE; break; case _SH_DENYWR: /* share read and delete access */ fileshare= FILE_SHARE_READ | FILE_SHARE_DELETE; break; case _SH_DENYRD: /* share write and delete access */ fileshare= FILE_SHARE_WRITE | FILE_SHARE_DELETE; break; case _SH_DENYNO: /* share read, write and delete access */ fileshare= FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE; break; case _SH_DENYRWD: /* exclusive access */ fileshare= 0L; break; case _SH_DENYWRD: /* share read access */ fileshare= FILE_SHARE_READ; break; case _SH_DENYRDD: /* share write access */ fileshare= FILE_SHARE_WRITE; break; case _SH_DENYDEL: /* share read and write access */ fileshare= FILE_SHARE_READ | FILE_SHARE_WRITE; break; default: /* error, bad shflag */ errno= EINVAL; DBUG_RETURN(-1); } /* decode open/create method flags */ switch (oflag & (_O_CREAT | _O_EXCL | _O_TRUNC)) { case 0: case _O_EXCL: /* ignore EXCL w/o CREAT */ filecreate= OPEN_EXISTING; break; case _O_CREAT: filecreate= OPEN_ALWAYS; break; case _O_CREAT | _O_EXCL: case _O_CREAT | _O_TRUNC | _O_EXCL: filecreate= CREATE_NEW; break; case _O_TRUNC: case _O_TRUNC | _O_EXCL: /* ignore EXCL w/o CREAT */ filecreate= TRUNCATE_EXISTING; break; case _O_CREAT | _O_TRUNC: filecreate= CREATE_ALWAYS; break; default: /* this can't happen ... all cases are covered */ errno= EINVAL; DBUG_RETURN(-1); } /* decode file attribute flags if _O_CREAT was specified */ fileattrib= FILE_ATTRIBUTE_NORMAL; /* default */ if (oflag & _O_CREAT) { _umask((mask= _umask(0))); if (!((pmode & ~mask) & _S_IWRITE)) fileattrib= FILE_ATTRIBUTE_READONLY; } /* Set temporary file (delete-on-close) attribute if requested. */ if (oflag & _O_TEMPORARY) { fileattrib|= FILE_FLAG_DELETE_ON_CLOSE; fileaccess|= DELETE; } /* Set temporary file (delay-flush-to-disk) attribute if requested.*/ if (oflag & _O_SHORT_LIVED) fileattrib|= FILE_ATTRIBUTE_TEMPORARY; /* Set sequential or random access attribute if requested. */ if (oflag & _O_SEQUENTIAL) fileattrib|= FILE_FLAG_SEQUENTIAL_SCAN; else if (oflag & _O_RANDOM) fileattrib|= FILE_FLAG_RANDOM_ACCESS; /* try to open/create the file */ if ((osfh= CreateFile(path, fileaccess, fileshare, &SecurityAttributes, filecreate, fileattrib, NULL)) == INVALID_HANDLE_VALUE) { /* OS call to open/create file failed! map the error, release the lock, and return -1. note that it's not necessary to call _free_osfhnd (it hasn't been used yet). */ my_osmaperr(GetLastError()); /* map error */ DBUG_RETURN(-1); /* return error to caller */ } if ((fh= my_open_osfhandle(osfh, oflag & (_O_APPEND | _O_RDONLY | _O_TEXT))) == -1) { CloseHandle(osfh); } DBUG_RETURN(fh); /* return handle */ }