Bug #21498
openWindows - Ruby Overrides C Library APIs thus breaking them
Description
I am trying to wrap a simple C++ library, https://github.com/baderouaich/BitmapPlusPlus, as a Ruby extension.
However when I use the extension to write a bitmap to disk the bitmap is corrupted. This is because the library uses std::ofstream which eventually uses the C API fclose to write the final bytes to the bitmap file and then closes it. The problem is that Ruby overrides fclose and replaces it with rb_w32_fclose. It then exports its custom version from from x64-vcruntime140-ruby340.dll. And the exported version is broken (at least from the standpoint of the C standard library).
Note this has been a long standing issue. The first report I see is from 2013:
https://bugs.ruby-lang.org/issues/8569
More recently in 2020 (which explains the issue very well):
https://github.com/NREL/OpenStudio/issues/3942#issuecomment-610673401
I understand that Ruby is trying to provide a platform independent API. But the problem is this solution breaks any third party libraries that rely on these C APIs (which of course are very common). And there is no good workaround (see https://github.com/NREL/OpenStudio/issues/3942#issuecomment-611072774).
So would it be possible for Ruby to stop exporting custom versions of basic C APIs? The code that does it is here:
https://github.com/ruby/ruby/blob/master/win32/mkexports.rb#L41
Ruby of course could still use its custom versions of fclose, read, write etc within ruby.exe and x64-vcruntime140-ruby340.dll. But they should not be exported from x64-vcruntime140-ruby340.dll and thus be off limits to extensions. If a case comes up where an extension really needs access to rb_w32_fclose instead of fclose then an extension developer can use an #ifdef _WIN32 to do so and work across platforms. That at least puts the developer in control versus now where I don't see any way I can wrap the bitmap library as a Ruby extension.
From my experience the biggest problem is the replacing of fclose with rb_w32_fclose.
This is the list of generated overrides:
FD_CLR=rb_w32_fdclr
FD_ISSET=rb_w32_fdisset
Sleep=rb_w32_Sleep
accept=rb_w32_accept
access=rb_w32_uaccess
bind=rb_w32_bind
close=rb_w32_close
connect=rb_w32_connect
dup2=rb_w32_dup2
fclose=rb_w32_fclose
fstat=rb_w32_fstati128
get_osfhandle=rb_w32_get_osfhandle
getcwd=rb_w32_ugetcwd
getenv=rb_w32_ugetenv
gethostbyaddr=rb_w32_gethostbyaddr
gethostbyname=rb_w32_gethostbyname
gethostname=rb_w32_gethostname
getpeername=rb_w32_getpeername
getpid=rb_w32_getpid
getppid=rb_w32_getppid
getprotobyname=rb_w32_getprotobyname
getprotobynumber=rb_w32_getprotobynumber
getservbyname=rb_w32_getservbyname
getservbyport=rb_w32_getservbyport
getsockname=rb_w32_getsockname
getsockopt=rb_w32_getsockopt
inet_ntop=rb_w32_inet_ntop
inet_pton=rb_w32_inet_pton
ioctlsocket=rb_w32_ioctlsocket
isatty=rb_w32_isatty
listen=rb_w32_listen
lseek=rb_w32_lseek
lstat=rb_w32_ulstati128
mkdir=rb_w32_umkdir
mmap=rb_w32_mmap
mprotect=rb_w32_mprotect
munmap=rb_w32_munmap
pipe=rb_w32_pipe
pread=rb_w32_pread
pwrite=rb_w32_pwrite
read=rb_w32_read
recv=rb_w32_recv
recvfrom=rb_w32_recvfrom
rename=rb_w32_urename
rmdir=rb_w32_urmdir
select=rb_w32_select
send=rb_w32_send
sendto=rb_w32_sendto
setsockopt=rb_w32_setsockopt
shutdown=rb_w32_shutdown
socket=rb_w32_socket
stati128=rb_w32_ustati128
strcasecmp=msvcrt.stricmp
strerror=rb_w32_strerror
strncasecmp=msvcrt.strnicmp
times=rb_w32_times
unlink=rb_w32_uunlink
utime=rb_w32_uutime
utimensat=rb_w32_uutimensat
utimes=rb_w32_uutimes
write=rb_w32_write
Files
Updated by cfis (Charlie Savage) 4 months ago
- Description updated (diff)
Updated by cfis (Charlie Savage) 4 months ago
- Description updated (diff)
Updated by cfis (Charlie Savage) 4 months ago
- Description updated (diff)
Updated by alanwu (Alan Wu) 3 months ago
- Assignee set to windows
Updated by alanwu (Alan Wu) 5 days ago
· Edited
Recent C runtimes on Windows have C99 compliant fclose(), and I think overriding it and breaks otherwise standard-compliant extensions. Removing the fclose() override passes CI, at least. https://github.com/ruby/ruby/pull/15073
@usa (Usaku NAKAMURA) What do you think?
Updated by cfis (Charlie Savage) 5 days ago
· Edited
Thanks @alanwu (Alan Wu). I added comments to the commit in Github, but copying here:
This MR will result in the Ruby dll exporting the symbol _fclose instead of fclose. The exporting happens here:
Microsoft tends to like to put _ in front of their c function names. In this case, fclose is ok but _flcoseall would not be. See https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/fclose-fcloseall?view=msvc-170.
So perhaps that regex should be updated to not export functions that have __ in their name?
Or, should rb_w32_fclose be entirely removed? If standard fclose works on windows now, then maybe it no longer serves a useful purpose?
Updated by usa (Usaku NAKAMURA) 5 days ago
@alanwu (Alan Wu)
I'm still a bit concerned about compatibility with existing extension libraries, but with Ruby4 about to be released, it might be a good time to make the change. So, I'm not opposed to it this time.
Updated by cfis (Charlie Savage) 4 days ago
· Edited
The universal crt was introduced by Microsoft in 2015 (https://learn.microsoft.com/en-us/cpp/porting/upgrade-your-code-to-the-universal-crt?view=msvc-170) and Ruby for Windows has used it since Ruby 3.1 in 2021 (https://rubyinstaller.org/2021/12/31/rubyinstaller-3.1.0-1-released.html). According to Microsoft it "conforms closely to the ISO C99 standard".
At this point in time, could Ruby drop some of its overrides on Windows. The UCRT function list is here:
The ones the UCRT implements that Ruby overrides are:
access=rb_w32_uaccess
close=rb_w32_close
dup2=rb_w32_dup2
fclose=rb_w32_fclose
fstat=rb_w32_fstati128 (_fstat)
get_osfhandle=rb_w32_get_osfhandle (_get_osfhandle)
getpid=rb_w32_getpid
isatty=rb_w32_isatty
lseek=rb_w32_lseek
mkdir=rb_w32_umkdir
pipe=rb_w32_pipe (_pipe)
read=rb_w32_read
rename=rb_w32_urename
rmdir=rb_w32_urmdir
strerror=rb_w32_strerror
unlink=rb_w32_uunlink
utime=rb_w32_uutime (_utime)
write=rb_w32_write
If there is interest, I can remove these overrides locally and see if Ruby still builds and what happens with tests.
Updated by nobu (Nobuyoshi Nakada) 4 days ago
I can't the exact cause how the problem occurs, maybe passing FILE* opened in other than Ruby to rb_w32_fclose?
usa (Usaku NAKAMURA) wrote in #note-7:
I'm still a bit concerned about compatibility with existing extension libraries, but with Ruby4 about to be released, it might be a good time to make the change. So, I'm not opposed to it this time.
Although it's questionable about that winsock functions and MSVC functions for file descriptors are incompatible, I'll not be against.
diff --git a/win32/mkexports.rb b/win32/mkexports.rb
index 1a9f474be28..734de9b8dd1 100755
--- a/win32/mkexports.rb
+++ b/win32/mkexports.rb
@@ -40,26 +40,11 @@
syms[internal] = export
winapis[$1] = internal if /^_?(rb_w32_\w+)(?:@\d+)?$/ =~ internal
end
- incdir = File.join(File.dirname(File.dirname(__FILE__)), "include/ruby")
- read_substitution(incdir+"/win32.h", syms, winapis)
- read_substitution(incdir+"/subst.h", syms, winapis)
syms["rb_w32_vsnprintf"] ||= "ruby_vsnprintf"
syms["rb_w32_snprintf"] ||= "ruby_snprintf"
@syms = syms
end
- def read_substitution(header, syms, winapis)
- File.foreach(header) do |line|
- if /^#define (\w+)\((.*?)\)\s+(?:\(void\))?(rb_w32_\w+)\((.*?)\)\s*$/ =~ line and
- $2.delete(" ") == $4.delete(" ")
- export, internal = $1, $3
- if syms[internal] or internal = winapis[internal]
- syms[forwarding(internal, export)] = internal
- end
- end
- end
- end
-
def exports(name = $name, library = $library, description = $description)
exports = []
if name
Updated by cfis (Charlie Savage) 4 days ago
In my case I created a Ruby extension based on https://github.com/baderouaich/BitmapPlusPlus. Every time I tried to save a bitmap though it was corrupted. I submitted a bug to the project - https://github.com/baderouaich/BitmapPlusPlus/issues/6.
But I later realized the real problem is Ruby overrides fclose:
- bitmap library uses
std::ofstreamto write the file - ofstream eventually calls
fclose
But instead of calling fclose in UCRT, it calls Ruby's version of fclose. That happens because the Ruby dll exports fclose as a symbol and that overrides the one exported from the C runtime library (UCRT). I know this for a fact because I stepped through the code using a debugger. I can show you a screenshot of that if you would like to see it yourself.
Ruby's version of fclose (rb32_fclose) is not compatible with the UCRT version, and thus the file ends up corrupted.
Updated by cfis (Charlie Savage) 4 days ago
· Edited
I have attached a screenshot showing the problem. Notice in the stack trace on the bottom of the page that the method bmp::Bitmap::save(const std::filesystem::path & filename) calls fclose but ends up calling the Ruby version! Not the UCRT version.
Updated by nobu (Nobuyoshi Nakada) about 9 hours ago
cfis (Charlie Savage) wrote in #note-11:
Notice in the stack trace on the bottom of the page that the method
bmp::Bitmap::save(const std::filesystem::path & filename)calls fclose but ends up calling the Ruby version! Not the UCRT version.
rb_w32_fclose also calls fclose in the UCRT.