You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Net::FTP has no #initialize: construction is wired to .new and to nothing else (-38 examples, the largest item left in net-ftp) #929
Net::FTP has no #initialize: construction is wired to .new and to nothing else
Measured against MRI 4.0.6 (net-ftp 0.3.9) over all 56 library/net-ftp files,
same corpus, 30 s per file:
engine
passing
MRI 4.0.6
459
rbgo
331
−128
initialize_spec.rb alone is −38 of that, and it is the largest single item
left in the module: MRI passes 41, rbgo passes 3.
The cause
Net::FTP#initialize is not defined. Construction lives entirely in the
class method Net::FTP.new, so the class inherits Object#initialize, which
takes no arguments:
RBGO_RESULT pass=3 fail=2 error=36 skip=0
RBGO_DETAIL error ArgumentError Net::FTP#initialize when passed host | tries to connect to the passed host
wrong number of arguments (given 1, expected 0)
ruby/spec drives initializedirectly, which is normal for this module —
it is how the suite tests construction without connecting:
This is the defect this repository keeps producing: a mechanism wired at only
some of its doors. Construction works perfectly through .new and does not
exist through #initialize, which is the door MRI documents as a private
instance method and which ruby/spec uses throughout.
It is the tenth instance recorded this week, after ioBlock in Net::HTTP but
not Net::FTP, read_timeout read from the options Hash but not open_timeout, and ftpInit returning before the options when the host was
nil.
Shape of the fix
Define Net::FTP#initialize(host = nil, options = {}) as a private instance
method carrying what ftpInit does today, and make Net::FTP.new allocate
and call it. Then .new, .open and send(:initialize, …) all go through one
implementation instead of one door being a special case.
Worth checking while there: MRI calls options.to_hash on the argument, so an
object that converts counts as an options Hash; rbgo currently requires a
literal Hash.
Not a hang
For the record, since this module is cited in #916: with a 30 s per-file limit, no file is killed by the clock under either engine — specdelta prints no KILLED on timeout row in this sweep. The remaining −128 is ordinary
unimplemented behaviour, not the hang symptom.
Prior art: priorart "Net::FTP#initialize private instance method send" at 2026-11-13 14:40 CET reported prior art; checked: priorart --repo go-embedded-ruby/ruby 'Net::FTP#initialize private instance method send' returned 508 broad keyword matches; the nearest are #916 (the net-ftp hang issue, which this explicitly is NOT), #928 (my own open PR, which adds default_passive and fixes ftpInit dropping options on a nil host but does not define #initialize), and #621 (an mspec shim gap). None covers Net::FTP#initialize being undefined.
$ rbgo -e 'require "net/ftp"; a = Net::FTP.allocate; p a.class; p a.passive'
Net::FTP
ArgumentError: passive: interface conversion: object.Value is *vm.RObject, not *vm.ftpObj
allocate hands back a generic *RObject that merely reports the class Net::FTP. Every accessor then fails its type assertion, so an #initialize
defined on top of it would be handed an object it cannot populate. The 38
examples in initialize_spec.rb all start from that allocate.
So the work is larger than "define a private #initialize":
a class whose instances are backed by a Go struct needs an allocator the
class itself provides, and Class#allocate has to consult it instead of
building a plain RObject;
then #initialize can be a private instance method carrying what ftpInit
does today, with .new becoming allocate-then-initialize;
and only then does send(:initialize, nil, options) reach a real ftpObj.
Step 1 is not specific to Net::FTP — every native-backed class has the same
hole, so it wants doing once in the object model rather than per binding. That
is a different and wider change than this issue was filed for, and worth saying
before someone starts at step 2.
⛔ A separate, smaller defect found on the way
The failure above is a Go interface-conversion message reaching Ruby:
ArgumentError: passive: interface conversion: object.Value is *vm.RObject, not *vm.ftpObj
Internal type names have no business in a user-visible exception, and the class
is wrong too — MRI would raise NoMethodError or TypeError here, not ArgumentError. Whatever happens to the rest of this issue, that message is
worth not shipping: it tells an embedder about *vm.ftpObj, and it will be
produced by any native-backed class reached through allocate.
Activity
tannevaled commentedon Nov 13, 2026
Before implementing this:
Net::FTP.allocatedoes not produce aNet::FTPDefining
#initializeis necessary and not sufficient, because of how thespec reaches it:
Measured on
main:allocatehands back a generic*RObjectthat merely reports the classNet::FTP. Every accessor then fails its type assertion, so an#initializedefined on top of it would be handed an object it cannot populate. The 38
examples in
initialize_spec.rball start from thatallocate.So the work is larger than "define a private
#initialize":class itself provides, and
Class#allocatehas to consult it instead ofbuilding a plain
RObject;#initializecan be a private instance method carrying whatftpInitdoes today, with
.newbecoming allocate-then-initialize;send(:initialize, nil, options)reach a realftpObj.Step 1 is not specific to
Net::FTP— every native-backed class has the samehole, so it wants doing once in the object model rather than per binding. That
is a different and wider change than this issue was filed for, and worth saying
before someone starts at step 2.
⛔ A separate, smaller defect found on the way
The failure above is a Go interface-conversion message reaching Ruby:
Internal type names have no business in a user-visible exception, and the class
is wrong too — MRI would raise
NoMethodErrororTypeErrorhere, notArgumentError. Whatever happens to the rest of this issue, that message isworth not shipping: it tells an embedder about
*vm.ftpObj, and it will beproduced by any native-backed class reached through
allocate.