├── .github └── workflows │ └── test.yml ├── .gitignore ├── .gitmodules ├── .jazzy.yaml ├── BUILDING.md ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── Package.swift ├── README.md ├── RubyGateway.xcconfig ├── RubyGateway.xcodeproj ├── RubyGatewayTests_Info.plist ├── RubyGateway_info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── RubyGateway-Package.xcscheme │ └── xcschememanagement.plist ├── SourceDocs ├── TODO.md └── User Guide.md ├── Sources ├── RubyGateway │ ├── CRubyMacros.swift │ ├── Lock.swift │ ├── RbBlockCall.swift │ ├── RbClass.swift │ ├── RbComplex.swift │ ├── RbConversions.swift │ ├── RbError.swift │ ├── RbFailableAccess.swift │ ├── RbGateway.swift │ ├── RbGlobalVar.swift │ ├── RbMethod.swift │ ├── RbNumericConversions.swift │ ├── RbObject.swift │ ├── RbObjectAccess.swift │ ├── RbObjectCollection.swift │ ├── RbOperators.swift │ ├── RbProc.swift │ ├── RbRational.swift │ ├── RbSymbol.swift │ ├── RbThread.swift │ ├── RbVM.swift │ └── String+RubyGateway.swift ├── RubyGatewayHelpers │ ├── include │ │ ├── module.modulemap │ │ └── rbg_helpers.h │ ├── rbg_macros.m │ ├── rbg_protect.m │ └── rbg_value.m └── RubyThreadSample │ ├── RubyExecutor.swift │ └── main.swift ├── Tests └── RubyGatewayTests │ ├── Fixtures │ ├── backwards.rb │ ├── demo.rb │ ├── endtoend.rb │ ├── inspectables.rb │ ├── methods.rb │ ├── nesting.rb │ ├── nonconvert.rb │ ├── numbers.rb │ ├── raising.rb │ ├── swift_classes.rb │ ├── swift_methods.rb │ ├── swift_obj_methods.rb │ └── unloadable.rb │ ├── Helpers.swift │ ├── TestArrays.swift │ ├── TestCallable.swift │ ├── TestClassDef.swift │ ├── TestCollection.swift │ ├── TestComplex.swift │ ├── TestConstants.swift │ ├── TestDemo.swift │ ├── TestDictionaries.swift │ ├── TestDynamic.swift │ ├── TestErrors.swift │ ├── TestFailable.swift │ ├── TestGlobalVars.swift │ ├── TestMethods.swift │ ├── TestMiscObjTypes.swift │ ├── TestNumerics.swift │ ├── TestObjMethods.swift │ ├── TestOperators.swift │ ├── TestProcs.swift │ ├── TestRanges.swift │ ├── TestRational.swift │ ├── TestRbObject.swift │ ├── TestSets.swift │ ├── TestStrings.swift │ ├── TestThreads.swift │ ├── TestVM.swift │ └── TestVars.swift └── docs ├── .nojekyll ├── Extensions ├── Array.html ├── ArraySlice.html ├── Bool.html ├── ClosedRange.html ├── Dictionary.html ├── Double.html ├── Float.html ├── Int.html ├── Int16.html ├── Int32.html ├── Int64.html ├── Int8.html ├── Range.html ├── Set.html ├── String.html ├── UInt.html ├── UInt16.html ├── UInt32.html ├── UInt64.html └── UInt8.html ├── Guides.html ├── badge.svg ├── css └── fw2020.css ├── docsets ├── RubyGateway.docset │ └── Contents │ │ ├── Info.plist │ │ └── Resources │ │ ├── Documents │ │ ├── Extensions │ │ │ ├── Array.html │ │ │ ├── ArraySlice.html │ │ │ ├── Bool.html │ │ │ ├── ClosedRange.html │ │ │ ├── Dictionary.html │ │ │ ├── Double.html │ │ │ ├── Float.html │ │ │ ├── Int.html │ │ │ ├── Int16.html │ │ │ ├── Int32.html │ │ │ ├── Int64.html │ │ │ ├── Int8.html │ │ │ ├── Range.html │ │ │ ├── Set.html │ │ │ ├── String.html │ │ │ ├── UInt.html │ │ │ ├── UInt16.html │ │ │ ├── UInt32.html │ │ │ ├── UInt64.html │ │ │ └── UInt8.html │ │ ├── Guides.html │ │ ├── badge.svg │ │ ├── css │ │ │ └── fw2020.css │ │ ├── error-handling.html │ │ ├── guides │ │ │ ├── todo.html │ │ │ └── user-guide.html │ │ ├── index.html │ │ ├── js │ │ │ ├── dependencies.min.js │ │ │ └── fw2020.js │ │ ├── main-apis.html │ │ ├── other-apis.html │ │ ├── search.json │ │ ├── site.json │ │ ├── swift-interop.html │ │ ├── swift-method-apis.html │ │ └── types │ │ │ ├── rbblockretention.html │ │ │ ├── rbbreak.html │ │ │ ├── rbcomplex.html │ │ │ ├── rberror.html │ │ │ ├── rberror │ │ │ └── history.html │ │ │ ├── rbexception.html │ │ │ ├── rbfailableaccess.html │ │ │ ├── rbgateway1.html │ │ │ ├── rbgateway1 │ │ │ └── verbosity.html │ │ │ ├── rbmethod.html │ │ │ ├── rbmethodargs.html │ │ │ ├── rbmethodargsspec.html │ │ │ ├── rbobject13.html │ │ │ ├── rbobjectaccess1.html │ │ │ ├── rbobjectcollection.html │ │ │ ├── rbobjectconvertible.html │ │ │ ├── rbproc.html │ │ │ ├── rbrational.html │ │ │ ├── rbsymbol.html │ │ │ ├── rbthread.html │ │ │ ├── rbthread │ │ │ └── unblockingfunc.html │ │ │ └── rbtype.html │ │ └── docSet.dsidx ├── RubyGateway.tgz └── RubyGateway.xml ├── error-handling.html ├── guides ├── todo.html └── user-guide.html ├── index.html ├── js ├── dependencies.min.js └── fw2020.js ├── main-apis.html ├── other-apis.html ├── search.json ├── site.json ├── swift-interop.html ├── swift-method-apis.html ├── types ├── rbblockretention.html ├── rbbreak.html ├── rbcomplex.html ├── rberror.html ├── rberror │ └── history.html ├── rbexception.html ├── rbfailableaccess.html ├── rbgateway1.html ├── rbgateway1 │ └── verbosity.html ├── rbmethod.html ├── rbmethodargs.html ├── rbmethodargsspec.html ├── rbobject13.html ├── rbobjectaccess1.html ├── rbobjectcollection.html ├── rbobjectconvertible.html ├── rbproc.html ├── rbrational.html ├── rbsymbol.html ├── rbthread.html ├── rbthread │ └── unblockingfunc.html └── rbtype.html └── unresolved.json /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | macos: 13 | name: macOS Xcode Toolchain 14 | runs-on: macos-15 15 | strategy: 16 | fail-fast: false 17 | matrix: 18 | xcode: ['16.3'] 19 | steps: 20 | - uses: maxim-lobanov/setup-xcode@v1 21 | with: 22 | xcode-version: ${{ matrix.xcode }} 23 | - uses: actions/checkout@v4 24 | with: 25 | submodules: recursive 26 | persist-credentials: false 27 | - name: Ruby 28 | run: | 29 | gem install rouge 30 | ruby --version 31 | ruby -e 'puts RbConfig::TOPDIR' 32 | ruby -e 'puts RbConfig::CONFIG' 33 | gem env 34 | - name: CRuby 35 | run: | 36 | swift package update 37 | swift package edit CRuby 38 | Packages/CRuby/cfg-cruby --mode custom --path `ruby -e 'puts RbConfig::TOPDIR'` 39 | cat Packages/CRuby/CRuby.pc 40 | cat Packages/CRuby/Sources/CRuby/module.modulemap 41 | cat Packages/CRuby/Sources/CRuby/ruby_headers.h 42 | - name: Tests (SPM) 43 | run: | 44 | export PKG_CONFIG_PATH=$(pwd)/Packages/CRuby:$PKG_CONFIG_PATH 45 | swift test --enable-code-coverage 46 | - name: Coverage generation 47 | run: | 48 | xcrun llvm-cov export -format lcov .build/debug/RubyGatewayPackageTests.xctest/Contents/MacOS/RubyGatewayPackageTests -instr-profile .build/debug/codecov/default.profdata -ignore-filename-regex "(Test|checkouts)" > coverage.lcov 49 | - name: Coverage upload 50 | uses: codecov/codecov-action@v4 51 | with: 52 | files: ./coverage.lcov 53 | token: ${{ secrets.CODECOV_TOKEN }} 54 | verbose: true 55 | - name: Samples 56 | run: | 57 | export PKG_CONFIG_PATH=$(pwd)/Packages/CRuby:$PKG_CONFIG_PATH 58 | swift run RubyThreadSample 59 | - name: Tests (Xcodebuild) 60 | run: | 61 | CRuby/cfg-cruby --mode custom --path `ruby -e 'puts RbConfig::TOPDIR'` 62 | xcodebuild test -scheme RubyGateway-Package 63 | 64 | linux: 65 | name: ubuntu 66 | runs-on: ubuntu-22.04 67 | strategy: 68 | fail-fast: false 69 | matrix: 70 | rby: 71 | - short: '2.7' 72 | - short: '3.0' 73 | - short: '3.1' 74 | - short: '3.2' 75 | - short: '3.3' 76 | - short: '3.4' 77 | steps: 78 | - uses: actions/checkout@v4 79 | with: 80 | persist-credentials: false 81 | - uses: vapor/swiftly-action@v0.2 82 | with: 83 | toolchain: "6.1" 84 | - uses: ruby/setup-ruby@v1 85 | with: 86 | ruby-version: ${{ matrix.rby.short }} 87 | - name: Ruby 88 | run: | 89 | gem install rouge 90 | ruby --version 91 | which ruby 92 | echo "RB_PREFIX=$(ruby -e 'puts RbConfig::TOPDIR')" >> $GITHUB_ENV 93 | ruby -e 'puts RbConfig::TOPDIR' 94 | ruby -e 'puts RbConfig::CONFIG' 95 | gem env 96 | ls -l /opt/hostedtoolcache/Ruby 97 | - name: CRuby 98 | run: | 99 | swift package update 100 | swift package edit CRuby 101 | Packages/CRuby/cfg-cruby --mode custom --path ${RB_PREFIX} 102 | cat Packages/CRuby/CRuby.pc 103 | cat Packages/CRuby/Sources/CRuby/module.modulemap 104 | cat Packages/CRuby/Sources/CRuby/ruby_headers.h 105 | - name: Tests 106 | run: | 107 | export PKG_CONFIG_PATH=$(pwd)/Packages/CRuby:$PKG_CONFIG_PATH 108 | export LD_LIBRARY_PATH=${RB_PREFIX}/lib:$LD_LIBRARY_PATH 109 | swift test -Xcc -fmodules 110 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | build/ 4 | DerivedData 5 | *.coverage.txt 6 | /Packages 7 | *.pbxuser 8 | !default.pbxuser 9 | *.mode1v3 10 | !default.mode1v3 11 | *.mode2v3 12 | !default.mode2v3 13 | *.perspectivev3 14 | !default.perspectivev3 15 | xcuserdata 16 | *.xccheckout 17 | *.moved-aside 18 | *.xcuserstate 19 | *.xcscmblueprint 20 | *.hmap 21 | *.ipa 22 | Carthage/Build 23 | c/ 24 | *.swp 25 | Package.resolved 26 | coverage.lcov 27 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "CRuby"] 2 | path = CRuby 3 | url = https://github.com/johnfairh/CRuby.git 4 | -------------------------------------------------------------------------------- /.jazzy.yaml: -------------------------------------------------------------------------------- 1 | author: John Fairhurst 2 | author_url: http://github.com/johnfairh 3 | module: RubyGateway 4 | module_version: 6.1.1 5 | copyright: Distributed under the MIT license. Maintained by [John Fairhurst](mailto:johnfairh@gmail.com). 6 | readme: README.md 7 | github_url: https://github.com/johnfairh/RubyGateway 8 | github_file_prefix: https://github.com/johnfairh/RubyGateway/tree/main 9 | clean: true 10 | products: 11 | - docs 12 | - docset 13 | - unresolved-json 14 | - undocumented-json 15 | xcodebuild_arguments: 16 | - "-project" 17 | - "RubyGateway.xcodeproj" 18 | - "-scheme" 19 | - "RubyGateway-Package" 20 | - "-destination" 21 | - "platform=OS X,arch=x86_64" 22 | sdk: macosx 23 | theme: fw2020 24 | documentation: SourceDocs/*md 25 | topic_style: source-order-defs 26 | deployment_url: https://johnfairh.github.io/RubyGateway/ 27 | 28 | custom_categories: 29 | - name: Guides 30 | children: 31 | - User Guide 32 | - TODO 33 | 34 | - name: Main APIs 35 | abstract: These types form the main API to RubyGateway. 36 | children: 37 | - RbGateway 38 | - RbObject 39 | - RbObjectAccess 40 | - RbObjectCollection 41 | 42 | - name: Swift Method APIs 43 | abstract: These types are used to implement Ruby methods in Swift. 44 | children: 45 | - RbMethodArgsSpec 46 | - RbMethodCallback 47 | - RbBoundMethodCallback 48 | - RbBoundMethodVoidCallback 49 | - RbMethod 50 | - RbMethodArgs 51 | 52 | - name: Other APIs 53 | abstract: These types are used less often than those in [`Main APIs`](Main%20APIs.html). 54 | children: 55 | - RbBlockCallback 56 | - RbBlockRetention 57 | - RbBreak 58 | - RbProc 59 | - RbSymbol 60 | - RbThread 61 | - RbType 62 | 63 | - name: Error Handling 64 | abstract: These types are used to deal with error conditions. 65 | children: 66 | - RbError 67 | - RbException 68 | - RbFailableAccess 69 | 70 | - name: Swift Interop 71 | abstract: These types and extensions are used to convert Swift datatypes to Ruby and vice versa. 72 | children: 73 | - RbObjectConvertible 74 | - String 75 | - Bool 76 | - UInt 77 | - UInt64 78 | - UInt32 79 | - UInt16 80 | - UInt8 81 | - Int 82 | - Int64 83 | - Int32 84 | - Int16 85 | - Int8 86 | - Double 87 | - Float 88 | - Array 89 | - ArraySlice 90 | - Dictionary 91 | - Set 92 | - Range 93 | - ClosedRange 94 | - RbComplex 95 | - RbRational 96 | -------------------------------------------------------------------------------- /BUILDING.md: -------------------------------------------------------------------------------- 1 | Notes on how the framework is built. There is a certain amount of messing 2 | around to comply with the idiosyncracies of / my lack of patience with 3 | Swift Package Manager. 4 | 5 | ## Components 6 | 7 | * CRuby - system modulemap and headers for libruby 8 | * RubyGatewayHelpers - C code layer, depends on CRuby 9 | * RubyGateway - Swift layer, depends on CRuby and RubyGatewayHelpers. 10 | 11 | ## Goals 12 | 13 | * Users install just one thing (RubyGateway) 14 | * Users do not need any weird flags or settings. 15 | * Users get just the RubyGateway interface 16 | * Support Xcode/Carthage, SwiftPM (because Linux) 17 | 18 | Haven't managed to meet all these goals :-) 19 | 20 | ## Xcode/Carthage 21 | 22 | CRuby is a git submodule of RubyGateway, refer to it via Xcode options. 23 | 24 | RubyGatewayHelpers is a static library wrapped up in a modulemap. 25 | 26 | RubyGateway depends on CRuby and RubyGatewayHelpers as modules. 27 | 28 | Everything is awesome. 29 | 30 | ## Swift PM 31 | 32 | CRuby is a formal dependency from Package.swift. The CRuby submodule 33 | checkout is unused. 34 | 35 | RubyGatewayHelpers is a static lib, RubyGateway depends on them both like 36 | the Xcode version. 37 | 38 | Everything is fine. 39 | 40 | ## Ruby 3 notes 41 | 42 | Spell to get Swift docs for CRuby if Xcode can't do it: 43 | ```shell 44 | jazzy --min-acl private --module CRuby --swift-build-tool symbolgraph --build-tool-arguments -I,/Users/johnf/.rbenv/versions/3.0.0/include/ruby-3.0.0/x86_64-darwin20,-I,/Users/johnf/.rbenv/versions/3.0.0/include/ruby-3.0.0,-I,$(pwd),-Xcc,-fdeclspec 45 | ``` 46 | 47 | ## Releasing 48 | 49 | * Update changelog, .jazzy.yaml, TODO, README, LICENSE if year changed. 50 | * Update docs if needed, separate commit. 51 | * Commit + tag + push with `--tags`. Check CI. 52 | * Github code -> releases -> tags -> 'Create release' 53 | * Title is just release triple 54 | * Paste in changelog section 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 6.1.1 -- 10th February 2025 2 | 3 | * More robust error handling (#55, #58) 4 | * Stop calling `ruby_cleanup` at exit on thread 0 (#56) 5 | * Add sample code showing how to bind ruby to a non-main thread 6 | 7 | ## 6.1.0 -- 11th January 2025 8 | 9 | * Support Ruby 3.4 10 | 11 | ## 6.0.0 - 19th September 2024 12 | 13 | Swift 6 and concurrency support 14 | 15 | * More concurrency-correct API changes 16 | * Split "call-with-Swift-block" methods into two versions: one taking 17 | a non-escaping non-sendable closure for immediate evaluation, and 18 | the other taking a sendable, escaping closure for future use 19 | * Updated documentation for Linux workarounds 20 | 21 | ## 6.0.0-pre1 - 23rd April 2024 22 | 23 | First release leading up to Swift 6. 24 | 25 | * Revamp for modern Swift including concurrency 26 | checking, many technically-changed APIs 27 | * Update CRuby, introduce xcconfig 28 | 29 | ## 5.5.0 - 30th January 2024 30 | 31 | #### Enhancements 32 | 33 | * Support Ruby 3.3 34 | 35 | ## 5.4.0 - 27th January 2023 36 | 37 | #### Enhancements 38 | 39 | * Support Ruby 3.2 40 | 41 | ## 5.3.0 - 20th October 2022 42 | 43 | #### Enhancements 44 | 45 | * Support Ruby 3.1 46 | 47 | ## 5.2.0 - 2nd October 2021 48 | 49 | #### Enhancements 50 | 51 | * Support building cleanly with Xcode13 GA. 52 | [Karim Alweheshy](https://github.com/karimalweheshy) 53 | 54 | ## 5.1.0 - 2nd July 2021 55 | 56 | #### Breaking 57 | 58 | * Removed `RbGateway.taintChecks` -- `$SAFE` removed in Ruby 3 59 | * Internal modules `CRuby` and `RubyGatewayHelpers` are now imported as 60 | `@_implementationOnly` 61 | 62 | #### Enhancements 63 | 64 | * Support Ruby 3 - check README notes on `-fdeclspec`, see CI for an example 65 | * Support building cleanly with Xcode 13 66 | * Add `kwArgs` parameter to `RbMethod.yieldBlock(...)` 67 | 68 | ## 4.0.0 - 18th May 2021 69 | 70 | #### Breaking 71 | 72 | * Require minimum Swift 5.4 / Xcode 12.5 73 | * Require minimum Ruby 2.6 74 | 75 | ## 3.2.1 - 11th May 2020 76 | 77 | #### Bug Fixes 78 | 79 | * Fix warnings and tests for Swift 5.2/Xcode 11.4. 80 | 81 | ## 3.2.0 - 11th December 2019 82 | 83 | #### Breaking 84 | 85 | * None 86 | 87 | #### Enhancements 88 | 89 | * Add `RbObjectAccess.setConstant(_:newValue:)`, somehow overlooked! 90 | * Add `RbGateway.setArguments(_:)` to help with ARGV-setting. 91 | 92 | #### Bug Fixes 93 | 94 | * None 95 | 96 | ## 3.1.0 - 29th October 2019 97 | 98 | #### Enhancements 99 | 100 | * Add `Hashable` conformance to `RbSymbol`. 101 | * Tests pass on Ruby 2.6 / Xcode 11.2. 102 | 103 | ## 3.0.0 - 16th June 2019 104 | 105 | ##### Breaking 106 | 107 | * Require minimum Swift 5 / Xcode 10.2 / Ruby 2.3. 108 | * Standardize all APIs to not require a leading `name` arg label. 109 | * Retire @dynamicMemberLookup support now the level of support from Swift 110 | is clearer. May revisit this in future. 111 | 112 | ##### Enhancements 113 | 114 | * Implement class and singleton-class methods in Swift. 115 | * Define classes and modules from Swift. 116 | * Add module mix-in functions to `RbObject`. 117 | * Bind Ruby objects and methods directly to Swift objects and methods. 118 | * Add throwing conversion as alternative to optional initializer. 119 | * Add `RbMethod.callSuper()` to call superclass method. 120 | 121 | ##### Bug Fixes 122 | 123 | ## 2.1.0 - 18th December 2018 124 | 125 | ##### Enhancements 126 | 127 | * Implement global functions in Swift. 128 | 129 | ##### Bug Fixes 130 | 131 | * Ruby nil coerced to Dictionary should give empty not Swift nil. 132 | 133 | ## 2.0.0 - 8th October 2018 134 | 135 | ##### Breaking 136 | 137 | * Require Swift 4.2. 138 | 139 | ##### Enhancements 140 | 141 | * Dynamic member lookup for property access or 0-arg methods. 142 | * Global variables can use native Swift types. 143 | 144 | ## 1.1.0 - 18th July 2018 145 | 146 | * Add `RbComplex` wrapper for Ruby Complex. 147 | * Add `RbRational` wrapper for Ruby Rational. 148 | * Add `RbGateway.defineGlobalVar` - dynamically implement Ruby global 149 | variables in Swift. 150 | 151 | ## 1.0.0 - 11th May 2018 152 | 153 | * Add `RbGateway.taintChecks`. 154 | * Full SemVer rules from now on. 155 | 156 | ## 0.5.0 - 5th May 2018 157 | 158 | ##### Breaking 159 | 160 | * Change `kwArgs` to use `DictionaryLiteral` per dynamic callable. 161 | 162 | ##### Enhancements 163 | 164 | * Add conditional Set `RbObjectConvertible` conformance. 165 | * Add conditional ArraySlice `RbObjectConvertible` conformance. 166 | 167 | ## 0.4.0 - 20th April 2018 168 | 169 | ##### Breaking 170 | 171 | * Require Swift 4.1 (conditional conformances). 172 | * Replace `RbObject`'s `CustomPlaygroundQuickLookable` conformance with 173 | `CustomPlaygroundDisplayConvertible`. 174 | 175 | ##### Enhancements 176 | 177 | * Add conditional Array `RbObjectConvertible` conformance. 178 | * Add conditional Dictionary `RbObjectConvertible` conformance. 179 | * Add `RbThread` utilities and rules for multithreading. 180 | * Add conditional Range family `RbObjectConvertible` conformance. 181 | * Add `RbObjectCollection` to use Swift collection protocols with Ruby. 182 | * Allow Swift `nil` literal in argument positions to mean Ruby `nil`. 183 | 184 | ## 0.3.0 - 21st March 2018 185 | 186 | CocoaPods. 187 | 188 | ## 0.2.0 - 19th March 2018 189 | 190 | Add `RbProc` `RbBlockCallback` `RbBreak` and new `RbObjectAccess.call(...)` 191 | variants to let Swift code implement Ruby blocks. 192 | 193 | ## 0.1.0 - 12th March 2018 194 | 195 | Basic data types and object access. 196 | 197 | Swift PM and Carthage. 198 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Portions of CRubyMacros.swift are derived from the Ruby API header files 2 | under the Ruby license, see COPYING in the Ruby distribution. 3 | https://github.com/ruby/ruby/blob/trunk/COPYING 4 | 5 | The remainder of this software is licensed as follows. 6 | 7 | 8 | MIT License 9 | 10 | Copyright (c) 2018-2025 Matthew John Fairhurst 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining a copy 13 | of this software and associated documentation files (the "Software"), to deal 14 | in the Software without restriction, including without limitation the rights 15 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 16 | copies of the Software, and to permit persons to whom the Software is 17 | furnished to do so, subject to the following conditions: 18 | 19 | The above copyright notice and this permission notice shall be included in all 20 | copies or substantial portions of the Software. 21 | 22 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 23 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 24 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 25 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 26 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 27 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 28 | SOFTWARE. 29 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all build test 2 | 3 | swift_flags := -Xcc -fdeclspec 4 | 5 | pwd := $(shell pwd) 6 | 7 | all: build 8 | 9 | build: 10 | PKG_CONFIG_PATH=${pwd}/Packages/CRuby swift build ${swift_flags} 11 | 12 | test: 13 | PKG_CONFIG_PATH=${pwd}/Packages/CRuby swift test ${swift_flags} 14 | 15 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | 3 | // Package.swift 4 | // RubyGateway 5 | // 6 | // Distributed under the MIT license, see LICENSE 7 | 8 | import PackageDescription 9 | 10 | let package = Package( 11 | name: "RubyGateway", 12 | products: [ 13 | .library( 14 | name: "RubyGateway", 15 | targets: ["RubyGateway", "RubyGatewayHelpers"]), 16 | .executable( 17 | name: "RubyThreadSample", 18 | targets: ["RubyThreadSample"]) 19 | ], 20 | dependencies: [ 21 | .package(url: "https://github.com/johnfairh/CRuby", from: "2.1.0"), 22 | ], 23 | targets: [ 24 | .target( 25 | name: "RubyGateway", 26 | dependencies: ["RubyGatewayHelpers", "CRuby"], 27 | swiftSettings: [ 28 | .enableExperimentalFeature("AccessLevelOnImport"), 29 | .enableExperimentalFeature("StrictConcurrency") 30 | ] 31 | ), 32 | .target( 33 | name: "RubyGatewayHelpers", 34 | dependencies: ["CRuby"]), 35 | .testTarget( 36 | name: "RubyGatewayTests", 37 | dependencies: ["RubyGateway"], 38 | exclude: ["Fixtures"]), 39 | .executableTarget( 40 | name: "RubyThreadSample", 41 | dependencies: ["RubyGateway"]) 42 | ] 43 | ) 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # RubyGateway 8 | 9 | [![CI](https://travis-ci.org/johnfairh/RubyGateway.svg?branch=master)](https://travis-ci.org/johnfairh/RubyGateway) 10 | [![codecov](https://codecov.io/gh/johnfairh/RubyGateway/branch/master/graph/badge.svg)](https://codecov.io/gh/johnfairh/RubyGateway) 11 | [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 12 | ![Platforms](https://img.shields.io/badge/platform-macOS%20%7C%20linux-lightgrey.svg) 13 | ![License](https://cocoapod-badges.herokuapp.com/l/RubyGateway/badge.png) 14 | 15 | Embed Ruby in Swift: load Gems, run Ruby scripts, invoke APIs seamlessly in 16 | both directions. 17 | 18 | RubyGateway is a framework built on the Ruby C API that lets Swift programs 19 | running on macOS or Linux painlessly and safely run and interact with Ruby 20 | programs. It's easy to pass Swift values into Ruby and turn Ruby objects 21 | back into Swift types. 22 | 23 | RubyGateway lets you call any Ruby method from Swift, including passing Swift 24 | closures as blocks. It lets you define Ruby classes and methods that are 25 | implemented in Swift. 26 | 27 | See [CRuby](https://github.com/johnfairh/CRuby) if you are looking for a 28 | low-level Ruby C API wrapper. 29 | 30 | * [Examples](#examples) 31 | * [Documentation](#documentation) 32 | * [Requirements](#requirements) 33 | * [Installation](#installation) 34 | * [Contributions](#contributions) 35 | * [License](#license) 36 | 37 | ## Examples 38 | 39 | ### Services 40 | 41 | [Rouge](https://github.com/jneen/rouge) is a code highlighter. In Ruby: 42 | ```ruby 43 | require 'rouge' 44 | html = Rouge.highlight("let a = 3", "swift", "html") 45 | puts(html) 46 | ``` 47 | 48 | In Swift: 49 | ```swift 50 | import RubyGateway 51 | 52 | try Ruby.require(filename: "rouge") 53 | let html = try Ruby.get("Rouge").call("highlight", args: ["let a = 3", "swift", "html"]) 54 | print(html) 55 | ``` 56 | 57 | ### Calling Ruby 58 | 59 | ```swift 60 | // Create an object. Use keyword arguments with initializer 61 | let student = RbObject(ofClass: "Academy::Student", kwArgs: ["name": "barney"])! 62 | 63 | // Access an attribute 64 | print("Name is \(student.get("name"))") 65 | 66 | // Fix their name by poking an ivar 67 | try! student.setInstanceVar("@name", newValue: "Barney") 68 | 69 | // Get a Swift version of `:reading` 70 | let readingSubject = RbSymbol("reading") 71 | 72 | // Call a method with mixed Swift data types 73 | try! student.call("add_score", args: [readingSubject, 30]) 74 | try! student.call("add_score", args: [readingSubject, 35]) 75 | 76 | // Get a result as floating point 77 | let avgScoreObj = try! student.call("mean_score_for_subject", args: [readingSubject]) 78 | let avgScore = Double(avgScoreObj)! 79 | print("Mean score is \(avgScore)") 80 | 81 | // Pass Swift code as a block 82 | let scores = student.all_scores! 83 | scores.call("each") { args in 84 | print("Subject: \(args[0]) Score: \(args[1])") 85 | return .nilObject 86 | } 87 | 88 | // Convert to a Swift array 89 | let subjects = Array(student.all_subjects!) 90 | subjectsPopularityDb.submit(subjects: subjects) 91 | ``` 92 | 93 | ## Calling Swift 94 | 95 | Bound class definition: 96 | ```swift 97 | class Cell { 98 | init() { 99 | } 100 | 101 | func setup(m: RbMethod) throws { 102 | ... 103 | } 104 | 105 | func getContent(m: RbMethod) throws -> String { 106 | ... 107 | } 108 | } 109 | 110 | let cellClass = try Ruby.defineClass("Cell", initializer: Cell.init) 111 | 112 | try cellClass.defineMethod("initialize", 113 | argsSpec: RbMethodArgsSpec(mandatoryKeywords: ["width", "height"]) 114 | method: Cell.setup) 115 | 116 | try cellClass.defineMethod("content", 117 | argsSpec: RbMethodArgsSpec(requiresBlock: true), 118 | method: Cell.getContent) 119 | ``` 120 | Called from Ruby: 121 | ```ruby 122 | cell = Cell.new(width: 200, height: 100) 123 | cell.content { |c| prettyprint(c) } 124 | ``` 125 | 126 | Global variables: 127 | ```swift 128 | // epochStore.current: Int 129 | 130 | Ruby.defineGlobalVar("$epoch", 131 | get: { epochStore.current }, 132 | set: { epochStore.current = newEpoch }) 133 | ``` 134 | 135 | Global functions: 136 | ```swift 137 | let logArgsSpec = RbMethodArgsSpec(leadingMandatoryCount: 1, 138 | optionalKeywordValues: ["priority" : 0]) 139 | try Ruby.defineGlobalFunction("log", 140 | argsSpec: logArgsSpec) { _, method in 141 | Logger.log(message: String(method.args.mandatory[0]), 142 | priority: Int(method.args.keyword["priority"]!)) 143 | return .nilObject 144 | } 145 | ``` 146 | Calls from Ruby: 147 | ```ruby 148 | log(object_to_log) 149 | log(object2_to_log, priority: 2) 150 | ``` 151 | 152 | ## Documentation 153 | 154 | * [User guide](https://johnfairh.github.io/RubyGateway/guides/user-guide.html) 155 | * [API documentation](https://johnfairh.github.io/RubyGateway) 156 | * [Docset for Dash](https://johnfairh.github.io/RubyGateway/docsets/RubyGateway.tgz) 157 | 158 | ## Requirements 159 | 160 | * Swift 6.0 or later, from swift.org or Xcode 16+ 161 | * macOS (tested on 15.3) or Linux (tested on Ubuntu Jammy) 162 | * Ruby 2.6 or later including development files: 163 | * For macOS, these come with Xcode. 164 | * For Linux you may need to install a -dev package depending on how your Ruby 165 | is installed. 166 | * RubyGateway requires 'original' MRI/CRuby Ruby - no JRuby/Rubinius/etc. 167 | 168 | ## Installation 169 | 170 | For macOS, if you are happy to use the system Ruby then you just need to include 171 | the RubyGateway framework as a dependency. If you are building on Linux or want 172 | to use a different Ruby then you also need to configure CRuby. 173 | 174 | ## Linux 175 | 176 | As of Swift 6, Apple have broken Swift PM such that you must pass "-Xcc -fmodules" to build the project. Check the CI invocation for an example. 177 | 178 | ### Getting the framework 179 | 180 | Carthage for macOS: 181 | ``` 182 | github "johnfairh/RubyGateway" 183 | ``` 184 | 185 | Swift package manager for macOS or Linux: 186 | ``` 187 | .package(url: "https://github.com/johnfairh/RubyGateway", from: "6.1.0") 188 | ``` 189 | 190 | ### Configuring CRuby 191 | 192 | CRuby is the glue between RubyGateway and your Ruby installation. It is a 193 | [separate github project](https://github.com/johnfairh/CRuby) but RubyGateway 194 | includes it as submodule so you do not install or require it separately. 195 | 196 | By default it points to the macOS system Ruby. Follow the [CRuby usage 197 | instructions](https://github.com/johnfairh/CRuby#usage) to change 198 | this. For example on Ubuntu 18 using `rbenv` Ruby 3: 199 | ```shell 200 | mkdir MyProject && cd MyProject 201 | swift package init --type executable 202 | vi Package.swift 203 | # add RubyGateway as a package dependency (NOT CRuby) 204 | # add RubyGateway as a target dependency 205 | echo "import RubyGateway; print(Ruby.versionDescription)" > Sources/MyProject/main.swift 206 | swift package update 207 | swift package edit CRuby 208 | Packages/CRuby/cfg-cruby --mode rbenv --name 3.0.0 209 | PKG_CONFIG_PATH=$(pwd)/Packages/CRuby:$PKG_CONFIG_PATH swift run -Xcc -fmodules 210 | ``` 211 | 212 | ## Contributions 213 | 214 | Welcome: open an issue / johnfairh@gmail.com / @johnfairh@mastodon.social 215 | 216 | ## License 217 | 218 | Distributed under the MIT license. 219 | -------------------------------------------------------------------------------- /RubyGateway.xcconfig: -------------------------------------------------------------------------------- 1 | // Jump over to CRuby for the actual settings. 2 | // Relative include path means relative to the location of this file. 3 | #include? "CRuby/CRuby.xcconfig" 4 | -------------------------------------------------------------------------------- /RubyGateway.xcodeproj/RubyGatewayTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /RubyGateway.xcodeproj/RubyGateway_info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /RubyGateway.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /RubyGateway.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /RubyGateway.xcodeproj/xcshareddata/xcschemes/RubyGateway-Package.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 46 | 47 | 53 | 54 | 60 | 61 | 62 | 63 | 65 | 71 | 72 | 75 | 76 | 77 | 78 | 79 | 89 | 90 | 96 | 97 | 98 | 99 | 105 | 106 | 108 | 109 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /RubyGateway.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | TMLRuby-Package.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /SourceDocs/TODO.md: -------------------------------------------------------------------------------- 1 | ## One day 2 | 3 | Optimize GC interaction 4 | 5 | Crashiness 6 | * Policy or something to avoid crashes in operators 7 | 8 | Dynamic callable / member lookup. Swift is not going to support X.Y(a) where 9 | Y is dynamic so we will have to compromise somewhere with Ruby, probably by 10 | requiring () even for calling 0-args functions / accessing properties. 11 | -------------------------------------------------------------------------------- /Sources/RubyGateway/CRubyMacros.swift: -------------------------------------------------------------------------------- 1 | // 2 | // CRubyMacros.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // Portions of this file derived from Ruby and distributed under terms of its license. 7 | 8 | internal import CRuby 9 | internal import RubyGatewayHelpers 10 | 11 | // 12 | // Various useful stuff from ruby.h that didn't make it through the Clang importer. 13 | // 14 | // Mostly to do with numeric types, stuffing into and out of FIXNUM format, plus 15 | // type-reasoning macros. 16 | // 17 | // Began keeping this in the same order as ruby.h but good grief the numeric functions 18 | // drove me crazy this way -- so reordered to group all like stuff together. 19 | // 20 | // RubyGateway uses the NUM abstraction layers exclusively so most stuff lower than that 21 | // is omitted. 22 | // 23 | 24 | // MARK: - Integer conversions 25 | 26 | // From platform to NUM -- BIGNUM or FIXNUM depending on size. 27 | 28 | func RB_LONG2NUM(_ x: Int) -> VALUE { rb_long2num_inline(x) } 29 | func RB_ULONG2NUM(_ x: UInt) -> VALUE { rb_ulong2num_inline(x) } 30 | 31 | // MARK: - Floating point conversions 32 | 33 | // From platform to NUM -- FLONUM or CLASS(FLOAT) depending 34 | func DBL2NUM(_ dbl: Double) -> VALUE { rb_float_new(dbl) } 35 | 36 | // MARK: - Useful VALUE constants and macros 37 | 38 | let Qfalse: VALUE = VALUE(rbg_qfalse()) 39 | let Qtrue: VALUE = VALUE(rbg_qtrue()) 40 | let Qnil: VALUE = VALUE(rbg_qnil()) 41 | let Qundef: VALUE = VALUE(rbg_qundef()) 42 | 43 | // MARK: - More enum-y `VALUE` type enum 44 | 45 | // Swift-friendly value type. Constants duplicated from Ruby headers, 46 | // can't see how not to. Ruby 3 moves to an actual enum that Swift managers 47 | // to import so eventually this will go away. 48 | 49 | /// The type of a Ruby VALUE as wrapped by `RbObject`. 50 | /// 51 | /// Not generally useful, maybe for debugging. 52 | public enum RbType: Int32 { 53 | /// RUBY_T_NONE 54 | case T_NONE = 0x00 55 | 56 | /// RUBY_T_OBJECT 57 | case T_OBJECT = 0x01 58 | /// RUBY_T_CLASS 59 | case T_CLASS = 0x02 60 | /// RUBY_T_MODULE 61 | case T_MODULE = 0x03 62 | /// RUBY_T_FLOAT 63 | case T_FLOAT = 0x04 64 | /// RUBY_T_STRING 65 | case T_STRING = 0x05 66 | /// RUBY_T_REGEXP 67 | case T_REGEXP = 0x06 68 | /// RUBY_T_ARRAY 69 | case T_ARRAY = 0x07 70 | /// RUBY_T_HASH 71 | case T_HASH = 0x08 72 | /// RUBY_T_STRUCT 73 | case T_STRUCT = 0x09 74 | /// RUBY_T_BIGNUM 75 | case T_BIGNUM = 0x0a 76 | /// RUBY_T_FILE 77 | case T_FILE = 0x0b 78 | /// RUBY_T_DATA 79 | case T_DATA = 0x0c 80 | /// RUBY_T_MATCH 81 | case T_MATCH = 0x0d 82 | /// RUBY_T_COMPLEX 83 | case T_COMPLEX = 0x0e 84 | /// RUBY_T_RATIONAL 85 | case T_RATIONAL = 0x0f 86 | 87 | /// RUBY_T_NIL 88 | case T_NIL = 0x11 89 | /// RUBY_T_TRUE 90 | case T_TRUE = 0x12 91 | /// RUBY_T_FALSE 92 | case T_FALSE = 0x13 93 | /// RUBY_T_SYMBOL 94 | case T_SYMBOL = 0x14 95 | /// RUBY_T_FIXNUM 96 | case T_FIXNUM = 0x15 97 | /// RUBY_T_UNDEF 98 | case T_UNDEF = 0x16 99 | 100 | /// RUBY_T_IMEMO 101 | case T_IMEMO = 0x1a 102 | /// RUBY_T_NODE 103 | case T_NODE = 0x1b 104 | /// RUBY_T_ICLASS 105 | case T_ICLASS = 0x1c 106 | /// RUBY_T_ZOMBIE 107 | case T_ZOMBIE = 0x1d 108 | /// RUBY_T_MOVED 109 | case T_MOVED = 0x1e 110 | } 111 | 112 | func TYPE(_ x: VALUE) -> RbType { 113 | RbType(rawValue: rbg_type(x)) ?? .T_UNDEF 114 | } 115 | 116 | // MARK: - Garbage collection helpers 117 | 118 | //#define RB_OBJ_WB_UNPROTECT(x) rb_obj_wb_unprotect(x, __FILE__, __LINE__) 119 | //#define RB_OBJ_WRITE(a, slot, b) rb_obj_write((VALUE)(a), (VALUE *)(slot),(VALUE)(b), __FILE__, __LINE__) 120 | -------------------------------------------------------------------------------- /Sources/RubyGateway/Lock.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Lock.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import Foundation 9 | 10 | /// Dumb pthread wrapper. Ostrich approach to error handling. 11 | final class Lock { 12 | private var mutex = pthread_mutex_t() 13 | 14 | init(recursive: Bool = false) { 15 | if recursive { 16 | var attr = pthread_mutexattr_t() 17 | pthread_mutexattr_settype(&attr, Int32(PTHREAD_MUTEX_RECURSIVE)) 18 | pthread_mutex_init(&mutex, &attr) 19 | } else { 20 | pthread_mutex_init(&mutex, nil) 21 | } 22 | } 23 | 24 | func lock() { 25 | pthread_mutex_lock(&mutex) 26 | } 27 | 28 | func unlock() { 29 | pthread_mutex_unlock(&mutex) 30 | } 31 | 32 | func locked(call: () throws -> T) rethrows -> T { 33 | lock() 34 | defer { unlock() } 35 | return try call() 36 | } 37 | } 38 | 39 | /// This is used to hold lookups used to implement Ruby calling into Swift objects. 40 | /// Today, these calls all happen under the GVL so are inherently serialized. 41 | /// However it does no harm to add this extra level of checking and is a bit more 42 | /// obvious and future-proof in the Swift-6 world. 43 | final class LockedDictionary: @unchecked Sendable { 44 | private var dict: [Key: Value] 45 | private let lock: Lock 46 | 47 | init() { 48 | dict = [:] 49 | lock = Lock() 50 | } 51 | 52 | subscript(index: Key) -> Value? { 53 | set { 54 | lock.locked { 55 | newValue.map { dict[index] = $0 } 56 | } 57 | } 58 | get { 59 | lock.locked { 60 | dict[index] 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbBlockCall.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbBlockCall.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | internal import RubyGatewayHelpers 8 | 9 | // 10 | // This file deals with Ruby blocks. It gets a bit complicated. 11 | // 12 | // There are two slightly different codepaths: 13 | // 1) Call Ruby method passing Swift closure as block 14 | // * RbBlock.doBlockCall(...blockCall:) creates RbBlockContext 15 | // object to hold the closure. 16 | // * Call rb_block_call() with that object which ends up in 17 | // rbproc_pvoid_block_callback() whenever Ruby deigns to invoke 18 | // the block. 19 | // * Retrieve context, run Swift closure, handle exceptions etc. 20 | // * Pass control back to C code rbg_block_pvoid_callback() / 21 | // rbg_block_callback_tail(). 22 | // 23 | // 2) Call Ruby method passing a Ruby Proc object as a block. 24 | // * RbBlock.doBlockCall(...block:) uses Proc object as the 25 | // context, no extra object to worry about here. 26 | // * Call rb_block_call() with that object which ends up in 27 | // rbproc_value_block_callback() whenever Ruby deigns to invoke 28 | // the block. 29 | // * Retrieve context, invoke Proc, handle exceptions etc. 30 | // * Pass control back to C code rbg_block_value_callback() / 31 | // rbg_block_callback_tail(). 32 | // * This is messy because the API function that looks like it 33 | // should be used instead of this forwarding business, 34 | // `rb_funcall_with_block`, is `CALL_PUBLIC` instead of 35 | // `CALL_FCALL` like `rb_funcallv`. So yuck, we have to do this 36 | // proxy thing to provide similar level of function. 37 | // 38 | 39 | /// The type of a block implemented in Swift. 40 | /// 41 | /// The parameter is an array of arguments passed to the block. If the 42 | /// block has just one argument then this is still an array. 43 | /// 44 | /// The interface doesn't support other types of argument, for example keyword or 45 | /// optional. 46 | /// 47 | /// Blocks always return a value. You can use `RbObject.nilObject` if 48 | /// you have nothing useful to return. 49 | /// 50 | /// Blocks can throw errors: 51 | /// * Create and throw `RbBreak` instead of Ruby `break`; 52 | /// * Create and throw `RbException` instead of Ruby `raise`; 53 | /// * Throwing any other kind of error (including propagating `RbError`s) 54 | /// causes RubyGateway to convert the error into a Ruby RuntimeError 55 | /// exception and raise it. 56 | /// 57 | /// See `RbObjectAccess.call(_:args:kwArgs:blockRetention:blockCall:)` and 58 | /// `RbObject.init(blockCall:)`. 59 | public typealias RbBlockCallback = ([RbObject]) throws -> RbObject 60 | 61 | /// Control over how Swift closures passed as blocks are retained. 62 | /// 63 | /// When you pass a Swift closure as a block, for example using 64 | /// `RbObjectAccess.call(_:args:kwArgs:blockRetention:blockCall:)`, RubyGateway 65 | /// needs some help to understand how Ruby will use the closure. 66 | /// 67 | /// The easiest thing to get wrong is using `.none` when Ruby retains the 68 | /// block for use later. This causes a hard crash in `RbBlockContext.from(raw:)` 69 | /// when Ruby tries to call the block. 70 | public enum RbBlockRetention { 71 | /// Do not retain the closure. The default, appropriate when the block 72 | /// is used only during execution of the method it is passed to. For 73 | /// example `#each`. 74 | case none 75 | 76 | /// Retain the closure for as long as the object that owns the method. 77 | /// Use when a method stores a closure in an object property for later use. 78 | case `self` 79 | 80 | /// Retain the closure for as long as the object returned by the method. 81 | /// Use when the method is a factory that produces some object and passes 82 | /// that object the closure. For example `Proc#new`. 83 | case returned 84 | } 85 | 86 | // MARK: - Swift -> Ruby -> Swift call context 87 | 88 | /// Context passed to block callbacks wrapping up a Swift closure. 89 | /// 90 | /// This is the object that the retain policy is obsessed about. 91 | private class RbBlockContext { 92 | let callback: RbBlockCallback 93 | 94 | init(_ callback: @escaping RbBlockCallback) { 95 | self.callback = callback 96 | } 97 | 98 | /// Call a function passing it a `void *` representation of the `RbProcContext` 99 | func withRaw(callback: (UnsafeMutableRawPointer) throws -> T) rethrows -> T { 100 | let unmanaged = Unmanaged.passRetained(self) 101 | defer { unmanaged.release() } 102 | return try callback(unmanaged.toOpaque()) 103 | } 104 | 105 | /// Retrieve an `RbBlockContext` from its `void *` representation 106 | static func from(raw: UnsafeMutableRawPointer) -> RbBlockContext { 107 | // A EXC_BAD_ACCESS here usually means the blockRetention has been 108 | // set wrongly - at any rate, the `RbProcContext` has been deallocated 109 | // while Ruby was still using it. 110 | Unmanaged.fromOpaque(raw).takeUnretainedValue() 111 | } 112 | } 113 | 114 | /// The callback from Ruby for blocks implemented by Swift closures. 115 | /// 116 | /// VALUE scoping all a bit dodgy here but probably fine in practice... 117 | private func rbproc_pvoid_block_callback(rawContext: UnsafeMutableRawPointer, 118 | argc: Int32, argv: UnsafePointer, 119 | blockArg: VALUE, 120 | returnValue: UnsafeMutablePointer) { 121 | returnValue.setFrom { 122 | let context = RbBlockContext.from(raw: rawContext) 123 | var args: [RbObject] = [] 124 | for i in 0.., 137 | blockArg: VALUE, 138 | returnValue: UnsafeMutablePointer) { 139 | returnValue.setFrom { 140 | try RbVM.doProtect { tag in 141 | rbg_proc_call_with_block_protect(context, argc, argv, blockArg, &tag) 142 | } 143 | } 144 | } 145 | 146 | // MARK: - Utilities for setting up Proc callbacks 147 | 148 | /// Enum for namespace 149 | internal enum RbBlock { 150 | /// One-time init to register the callbacks 151 | private static let initOnce: Void = { 152 | rbg_register_pvoid_block_proc_callback(rbproc_pvoid_block_callback) 153 | rbg_register_value_block_proc_callback(rbproc_value_block_callback) 154 | }() 155 | 156 | /// Call a method on an object passing a Swift closure as its block 157 | internal static func doBlockCall(value: VALUE, 158 | methodId: ID, 159 | argValues: [VALUE], 160 | hasKwArgs: Bool, 161 | blockCall: @escaping RbBlockCallback) throws -> (AnyObject, VALUE) { 162 | let _ = initOnce 163 | let context = RbBlockContext(blockCall) 164 | return (context, try context.withRaw { rawContext in 165 | try RbVM.doProtect { tag in 166 | rbg_block_call_pvoid_protect(value, methodId, 167 | Int32(argValues.count), argValues, 168 | hasKwArgs ? 1 : 0, 169 | rawContext, &tag) 170 | } 171 | }) 172 | } 173 | 174 | /// Call a method on an object passing a Ruby object as its block 175 | internal static func doBlockCall(value: VALUE, 176 | methodId: ID, 177 | argValues: [VALUE], 178 | hasKwArgs: Bool, 179 | block: VALUE) throws -> VALUE { 180 | let _ = initOnce 181 | return try RbVM.doProtect { tag in 182 | rbg_block_call_value_protect(value, methodId, 183 | Int32(argValues.count), argValues, 184 | hasKwArgs ? 1 : 0, 185 | block, &tag) 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbComplex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbComplex.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | /// A simple interface to Ruby's complex number support. 9 | /// 10 | /// This is not a Swift complex number library. It could be used 11 | /// as an interface between one such and Ruby. 12 | /// 13 | /// Ruby always represents complex numbers internally using rectangular coordinates 14 | /// so this type does not offer any direct support for polar coordinates. 15 | /// 16 | public struct RbComplex: RbObjectConvertible { 17 | /// The real part of the complex number. 18 | public let real: Double 19 | /// The imaginary part of the complex number. 20 | public let imaginary: Double 21 | 22 | /// Create a new complex number from real and imaginary parts. 23 | public init(real: Double, imaginary: Double) { 24 | self.real = real 25 | self.imaginary = imaginary 26 | } 27 | 28 | /// Create a complex number from a Ruby object. 29 | /// 30 | /// This calls `#to_c` before extracting real and imaginary parts so can 31 | /// be passed various types of Ruby object. 32 | /// 33 | /// Returns `nil` if the object cannot be converted or if its real and 34 | /// imaginary parts cannot be converted to Swift `Double`s. 35 | /// See `RbError.history` to see why a conversion failed. 36 | public init?(_ value: RbObject) { 37 | guard let complex_obj = try? value.call("to_c"), 38 | let real_obj = try? complex_obj.call("real"), 39 | let imaginary_obj = try? complex_obj.call("imaginary"), 40 | let real = Double(real_obj), 41 | let imaginary = Double(imaginary_obj) else { 42 | return nil 43 | } 44 | self.real = real 45 | self.imaginary = imaginary 46 | } 47 | 48 | /// Convert some Swift data type to a complex number. 49 | /// 50 | /// This is a convenience wrapper that lets you access Ruby's 51 | /// library directly from Swift types, for example: 52 | /// ```swift 53 | /// let compl = RbComplex("1+2.3i") 54 | /// ``` 55 | public init?(_ value: any RbObjectConvertible) { 56 | self.init(value.rubyObject) 57 | } 58 | 59 | /// Get a Ruby version of an `RbComplex`. 60 | /// 61 | /// This can theoretically produce `RbObject.nilObject` if the `Complex` 62 | /// class has been nobbled in some way. 63 | public var rubyObject: RbObject { 64 | guard Ruby.softSetup(), 65 | let complexClass = try? Ruby.get("Complex"), 66 | let complexObject = try? complexClass.call("rectangular", args: [real, imaginary]) else { 67 | return .nilObject 68 | } 69 | return complexObject 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbGlobalVar.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbObjectGVar.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | internal import RubyGatewayHelpers 8 | 9 | // Some simple thunking code to wrap up rb_gvar_* code. 10 | // 11 | // We support only 'virtual' gvars which are the most general kind - the 'bound' 12 | // style doesn't work so well with our immutable `RbObject` pattern. 13 | 14 | // MARK: Callbacks from C code (rbg_value.m) 15 | 16 | private func rbobject_gvar_get_callback(id: ID) -> VALUE { 17 | RbGlobalVar.get(id: id) 18 | } 19 | 20 | private func rbobject_gvar_set_callback(id: ID, 21 | newValue: VALUE, 22 | returnValue: UnsafeMutablePointer) { 23 | RbGlobalVar.set(id: id, newValue: newValue, returnValue: returnValue) 24 | } 25 | 26 | private enum RbGlobalVar { 27 | 28 | /// One-time init to register the callbacks 29 | private static let initOnce: Void = { 30 | rbg_register_gvar_callbacks(rbobject_gvar_get_callback, rbobject_gvar_set_callback) 31 | }() 32 | 33 | /// Callbacks + store - type-erased at this point 34 | private struct Context { 35 | let get: () -> RbObject 36 | let set: ((RbObject) throws -> Void)? 37 | } 38 | 39 | private static let contexts = LockedDictionary() 40 | 41 | /// Create thunks to 42 | static func create(name: String, 43 | get: @escaping () -> T, 44 | set: ((T) throws -> Void)?) { 45 | let _ = initOnce 46 | let id = rbg_create_virtual_gvar(name, set == nil ? 1 : 0) 47 | let getter = { get().rubyObject } 48 | if let set = set { 49 | contexts[id] = 50 | Context(get: getter, 51 | set: { newRbObject in 52 | guard let typed = T(newRbObject) else { 53 | throw RbException(message: "Bad type of \(newRbObject) expected \(T.self)") 54 | } 55 | try set(typed) 56 | } 57 | ) 58 | } 59 | else { 60 | contexts[id] = Context(get: getter, set: nil) 61 | } 62 | } 63 | 64 | fileprivate static func get(id: ID) -> VALUE { 65 | if let context = contexts[id] { 66 | let object = context.get() 67 | return object.withRubyValue { $0 } 68 | } 69 | return Qnil // practically unreachable 70 | } 71 | 72 | fileprivate static func set(id: ID, newValue: VALUE, returnValue: UnsafeMutablePointer) { 73 | if let context = contexts[id], 74 | let setter = context.set { 75 | returnValue.setFrom { 76 | try setter(RbObject(rubyValue: newValue)) 77 | return Qnil 78 | } 79 | } 80 | } 81 | } 82 | 83 | // MARK: Swift Global Variables 84 | 85 | extension RbGateway { 86 | /// Create a readonly Ruby global variable implemented by Swift code. 87 | /// 88 | /// If your global variable is not a simple Swift value type then use `RbObject` 89 | /// as the closure return type. 90 | /// 91 | /// - parameters: 92 | /// - name: The name of the global variable. Must begin with `$`. Any existing global 93 | /// variable with this name is overwritten. 94 | /// - get: Function called whenever Ruby code reads the global variable. 95 | /// - throws: `RbError.badIdentifier(type:id:)` if `name` is bad; some other kind of error if Ruby is 96 | /// not working. 97 | public func defineGlobalVar(_ name: String, 98 | get: @escaping @Sendable () -> T) throws { 99 | try setup() 100 | try name.checkRubyGlobalVarName() 101 | RbGlobalVar.create(name: name, get: get, set: nil) 102 | } 103 | 104 | /// Create a read-write Ruby global variable implemented by Swift code. 105 | /// 106 | /// Errors thrown from the setter closure propagate into Ruby as exceptions. Ruby does 107 | /// not permit getters to raise exceptions. 108 | /// 109 | /// If your global variable is not a simple Swift value type then use `RbObject` as the 110 | /// closure return/argument type; you can manually throw an `RbException` from the setter 111 | /// if the provided Ruby value is the wrong shape. 112 | /// 113 | /// - parameters: 114 | /// - name: The name of the global variable. Must begin with `$`. Any existing global 115 | /// variable with this name is overwritten. 116 | /// - get: Function called whenever Ruby code reads the global variable. 117 | /// - set: Function called whenever Ruby code writes the global variable. 118 | /// - throws: `RbError.badIdentifier(type:id:)` if `name` is bad; some other kind of error if Ruby is 119 | /// not working. 120 | public func defineGlobalVar(_ name: String, 121 | get: @escaping @Sendable () -> T, 122 | set: @escaping @Sendable (T) throws -> Void) throws { 123 | try setup() 124 | try name.checkRubyGlobalVarName() 125 | RbGlobalVar.create(name: name, get: get, set: set) 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbNumericConversions.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbNumericConversions.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | // This file has tedious repetitions of the numeric convertible conformance 9 | // and their doc comments for the fixed-width integer types and float. 10 | 11 | // MARK: - Fixed-width Unsigned Integer 12 | 13 | extension UInt64: RbObjectConvertible { 14 | /// Try to get a 64-bit unsigned integer representation of an `RbObject`. 15 | /// 16 | /// It fails if the Ruby value: 17 | /// 1. Is numeric and negative; or 18 | /// 2. Is numeric, positive, and does not fit into the Swift type; or 19 | /// 3. Cannot be made into a suitable numeric via `to_int` or `to_i`. 20 | /// 21 | /// See `RbError.history` to find out why a conversion failed. 22 | /// 23 | /// If the Ruby value is floating-point then the integer part is used. 24 | public init?(_ value: RbObject) { 25 | guard let actual = UInt(value) else { 26 | return nil 27 | } 28 | self.init(actual) 29 | } 30 | 31 | /// A Ruby object for the number. 32 | public var rubyObject: RbObject { 33 | UInt(self).rubyObject 34 | } 35 | } 36 | 37 | extension UInt32: RbObjectConvertible { 38 | /// Try to get a 32-bit unsigned integer representation of an `RbObject`. 39 | /// 40 | /// It fails if the Ruby value: 41 | /// 1. Is numeric and negative; or 42 | /// 2. Is numeric, positive, and does not fit into the Swift type; or 43 | /// 3. Cannot be made into a suitable numeric via `to_int` or `to_i`. 44 | /// 45 | /// See `RbError.history` to find out why a conversion failed. 46 | /// 47 | /// If the Ruby value is floating-point then the integer part is used. 48 | public init?(_ value: RbObject) { 49 | guard let actual = UInt(value) else { 50 | return nil 51 | } 52 | self.init(exactly: actual) 53 | } 54 | 55 | /// A Ruby object for the number. 56 | public var rubyObject: RbObject { 57 | UInt(self).rubyObject 58 | } 59 | } 60 | 61 | extension UInt16: RbObjectConvertible { 62 | /// Try to get a 16-bit unsigned integer representation of an `RbObject`. 63 | /// 64 | /// It fails if the Ruby value: 65 | /// 1. Is numeric and negative; or 66 | /// 2. Is numeric, positive, and does not fit into the Swift type; or 67 | /// 3. Cannot be made into a suitable numeric via `to_int` or `to_i`. 68 | /// 69 | /// See `RbError.history` to find out why a conversion failed. 70 | /// 71 | /// If the Ruby value is floating-point then the integer part is used. 72 | public init?(_ value: RbObject) { 73 | guard let actual = UInt(value) else { 74 | return nil 75 | } 76 | self.init(exactly: actual) 77 | } 78 | 79 | /// A Ruby object for the number. 80 | public var rubyObject: RbObject { 81 | UInt(self).rubyObject 82 | } 83 | } 84 | 85 | extension UInt8: RbObjectConvertible { 86 | /// Try to get an 8-bit unsigned integer representation of an `RbObject`. 87 | /// 88 | /// It fails if the Ruby value: 89 | /// 1. Is numeric and negative; or 90 | /// 2. Is numeric, positive, and does not fit into the Swift type; or 91 | /// 3. Cannot be made into a suitable numeric via `to_int` or `to_i`. 92 | /// 93 | /// See `RbError.history` to find out why a conversion failed. 94 | /// 95 | /// If the Ruby value is floating-point then the integer part is used. 96 | public init?(_ value: RbObject) { 97 | guard let actual = UInt(value) else { 98 | return nil 99 | } 100 | self.init(exactly: actual) 101 | } 102 | 103 | /// A Ruby object for the number. 104 | public var rubyObject: RbObject { 105 | UInt(self).rubyObject 106 | } 107 | } 108 | 109 | // MARK: - Fixed-width Signed Integer 110 | 111 | extension Int64: RbObjectConvertible { 112 | /// Try to get a 64-bit signed integer representation of an `RbObject`. 113 | /// 114 | /// It fails if the Ruby value: 115 | /// 1. Is numeric and does not fit into the Swift type; or 116 | /// 2. Cannot be made into a suitable numeric via `to_int` or `to_i`. 117 | /// 118 | /// See `RbError.history` to find out why a conversion failed. 119 | /// 120 | /// If the Ruby value is floating-point then the integer part is used. 121 | public init?(_ value: RbObject) { 122 | guard let actual = Int(value) else { 123 | return nil 124 | } 125 | self.init(exactly: actual) 126 | } 127 | 128 | /// A Ruby object for the number. 129 | public var rubyObject: RbObject { 130 | Int(self).rubyObject 131 | } 132 | } 133 | 134 | extension Int32: RbObjectConvertible { 135 | /// Try to get a 32-bit signed integer representation of an `RbObject`. 136 | /// 137 | /// It fails if the Ruby value: 138 | /// 1. Is numeric and does not fit into the Swift type; or 139 | /// 2. Cannot be made into a suitable numeric via `to_int` or `to_i`. 140 | /// 141 | /// See `RbError.history` to find out why a conversion failed. 142 | /// 143 | /// If the Ruby value is floating-point then the integer part is used. 144 | public init?(_ value: RbObject) { 145 | guard let actual = Int(value) else { 146 | return nil 147 | } 148 | self.init(exactly: actual) 149 | } 150 | 151 | /// A Ruby object for the number. 152 | public var rubyObject: RbObject { 153 | Int(self).rubyObject 154 | } 155 | } 156 | 157 | extension Int16: RbObjectConvertible { 158 | /// Try to get a 16-bit signed integer representation of an `RbObject`. 159 | /// 160 | /// It fails if the Ruby value: 161 | /// 1. Is numeric and does not fit into the Swift type; or 162 | /// 2. Cannot be made into a suitable numeric via `to_int` or `to_i`. 163 | /// 164 | /// See `RbError.history` to find out why a conversion failed. 165 | /// 166 | /// If the Ruby value is floating-point then the integer part is used. 167 | public init?(_ value: RbObject) { 168 | guard let actual = Int(value) else { 169 | return nil 170 | } 171 | self.init(exactly: actual) 172 | } 173 | 174 | /// A Ruby object for the number. 175 | public var rubyObject: RbObject { 176 | Int(self).rubyObject 177 | } 178 | } 179 | 180 | extension Int8: RbObjectConvertible { 181 | /// Try to get an 8-bit signed integer representation of an `RbObject`. 182 | /// 183 | /// It fails if the Ruby value: 184 | /// 1. Is numeric and does not fit into the Swift type; or 185 | /// 2. Cannot be made into a suitable numeric via `to_int` or `to_i`. 186 | /// 187 | /// See `RbError.history` to find out why a conversion failed. 188 | /// 189 | /// If the Ruby value is floating-point then the integer part is used. 190 | public init?(_ value: RbObject) { 191 | guard let actual = Int(value) else { 192 | return nil 193 | } 194 | self.init(exactly: actual) 195 | } 196 | 197 | /// A Ruby object for the number. 198 | public var rubyObject: RbObject { 199 | Int(self).rubyObject 200 | } 201 | } 202 | 203 | // MARK: - Float 204 | 205 | extension Float: RbObjectConvertible { 206 | /// Try to get a `Float` floating-point representation of an `RbObject`. 207 | /// 208 | /// It fails if the Ruby value: 209 | /// 1. Is numeric and does not fit into the Swift type; or 210 | /// 2. Cannot be made into a suitable numeric via `to_f`. 211 | /// 212 | /// See `RbError.history` to find out why a conversion failed. 213 | /// 214 | /// Flavors of NaN are not preserved across the Ruby<->Swift interface. 215 | public init?(_ value: RbObject) { 216 | guard let actual = Double(value) else { 217 | return nil 218 | } 219 | if actual.isNaN { 220 | self.init() 221 | self = .nan 222 | } else { 223 | self.init(exactly: actual) 224 | } 225 | } 226 | 227 | /// A Ruby object for the number. 228 | public var rubyObject: RbObject { 229 | Double(self).rubyObject 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbObjectCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbObjectCollection.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | /// A view onto a Ruby array using Swift collection protocols. 9 | /// 10 | /// This is an adapter type that wraps an `RbObject` and adopts 11 | /// Swift collection protocols for use with an underlying Ruby 12 | /// array (or any Ruby object that supports `length`, `[]`, and 13 | /// `[]=`.) 14 | /// 15 | /// For example: 16 | /// ```swift 17 | /// myObj.collection.replaceSubrange(lower.. RbObject { 57 | get { 58 | rubyObject[index] 59 | } 60 | set { 61 | rubyObject[index] = newValue 62 | } 63 | } 64 | 65 | public func index(after i: Int) -> Int { 66 | return i + 1 67 | } 68 | 69 | public mutating func replaceSubrange(_ subrange: Range, with newElements: C) where C: Collection, C.Element: RbObjectConvertible { 70 | let newArray = Array(newElements) 71 | 72 | rubyObject[subrange.rubyObject] = RbObject(newArray) 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbOperators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbOperators.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | // This file provides conformances and so on to let users treat `RbObject`s as 9 | // number-like things, forwarding on to Ruby methods for +-*/ and enabling 10 | // various standard library etc. usages of `Numeric` and `SignedNumeric`. 11 | // 12 | // I'm not certain this is useful or wise, mostly because the underlying Ruby 13 | // objects could do anything. But, mostly it will not and the convenience of 14 | // working directly is probably worth it. 15 | // 16 | // More concerning is the lack of error handling - need to refactor in future 17 | // similar to `Hashable` etc. to enable less shakey policy. 18 | 19 | // MARK: - SignedNumeric 20 | 21 | extension RbObject: SignedNumeric { 22 | /// Create a Ruby object from some type conforming to `BinaryInteger` 23 | public convenience init(exactly value: T) { 24 | self.init(Int(value)) 25 | } 26 | 27 | /// Type to express the magnitude of a signed number. :nodoc: 28 | public typealias Magnitude = RbObject 29 | 30 | /// Subtraction operator for `RbObject`s. 31 | /// 32 | /// - note: Calls Ruby `-` method. Crashes the process (`fatalError`) 33 | /// if the objects do not support subtraction. 34 | public static func -(lhs: RbObject, rhs: RbObject) -> RbObject { 35 | do { 36 | return try lhs.call("-", args: [rhs]) 37 | } catch { 38 | fatalError("Calling '-' on \(lhs) with \(rhs) failed: \(error)") 39 | } 40 | } 41 | 42 | /// Addition operator for `RbObject`s. 43 | /// 44 | /// - note: Calls Ruby `+` method. Crashes the process (`fatalError`) 45 | /// if the objects do not support addition. 46 | public static func +(lhs: RbObject, rhs: RbObject) -> RbObject { 47 | do { 48 | return try lhs.call("+", args: [rhs]) 49 | } catch { 50 | fatalError("Calling '+' on \(lhs) with \(rhs) failed: \(error)") 51 | } 52 | } 53 | 54 | /// Multiplication operator for `RbObject`s. 55 | /// 56 | /// - note: Calls Ruby `*` method. Crashes the process (`fatalError`) 57 | /// if the objects do not support multiplication. 58 | public static func *(lhs: RbObject, rhs: RbObject) -> RbObject { 59 | do { 60 | return try lhs.call("*", args: [rhs]) 61 | } catch { 62 | fatalError("Calling '*' on \(lhs) with \(rhs) failed: \(error)") 63 | } 64 | } 65 | 66 | /// Division operator for `RbObject`s. 67 | /// 68 | /// - note: Calls Ruby `/` method. Crashes the process (`fatalError`) 69 | /// if the objects do not support division. 70 | public static func /(lhs: RbObject, rhs: RbObject) -> RbObject { 71 | do { 72 | return try lhs.call("/", args: [rhs]) 73 | } catch { 74 | fatalError("Calling '/' on \(lhs) with \(rhs) failed: \(error)") 75 | } 76 | } 77 | 78 | /// Remainder operator for `RbObject`s. 79 | /// 80 | /// - note: Calls Ruby `%` method. Crashes the process (`fatalError`) 81 | /// if the objects do not support remaindering. 82 | public static func %(lhs: RbObject, rhs: RbObject) -> RbObject { 83 | do { 84 | return try lhs.call("%", args: [rhs]) 85 | } catch { 86 | fatalError("Calling '%' on \(lhs) with \(rhs) failed: \(error)") 87 | } 88 | } 89 | 90 | // OK why do these not have default implementations? 91 | 92 | /// Addition-assignment operator for `RbObject`s. 93 | public static func +=(lhs: inout RbObject, rhs: RbObject) { 94 | lhs = lhs + rhs 95 | } 96 | 97 | /// Subtraction-assignment operator for `RbObject`s. 98 | public static func -=(lhs: inout RbObject, rhs: RbObject) { 99 | lhs = lhs - rhs 100 | } 101 | 102 | /// Multiplication-assignment operator for `RbObject`s. 103 | public static func *=(lhs: inout RbObject, rhs: RbObject) { 104 | lhs = lhs * rhs 105 | } 106 | 107 | /// Division-assignment operator for `RbObject`s. 108 | public static func /=(lhs: inout RbObject, rhs: RbObject) { 109 | lhs = lhs / rhs 110 | } 111 | 112 | /// Remainder-assignment operator for `RbObject`s. 113 | public static func %=(lhs: inout RbObject, rhs: RbObject) { 114 | lhs = lhs % rhs 115 | } 116 | 117 | /// The magnitude of the value. 118 | /// 119 | /// - note: Calls Ruby `magnitude` method. Crashes the process (`fatalError`) 120 | /// if the object does not support magnitude. 121 | public var magnitude: RbObject { 122 | do { 123 | return try call("magnitude") 124 | } catch { 125 | fatalError("Calling 'magnitude' on \(self) failed: \(error)") 126 | } 127 | } 128 | 129 | /// The negated version of the value. 130 | /// 131 | /// - note: Calls Ruby unary - method. Crashes the process (`fatalError`) 132 | /// if the object does not support this. 133 | public static prefix func -(_ operand: RbObject) -> RbObject { 134 | do { 135 | return try operand.call("-@") 136 | } catch { 137 | fatalError("Calling '-@' on \(operand) failed: \(error)") 138 | } 139 | } 140 | 141 | /// Unary plus operator. 142 | /// 143 | /// - note: Calls Ruby unary + method. Crashes the process (`fatalError`) 144 | /// if the object does not support this. 145 | public static prefix func +(_ operand: RbObject) -> RbObject { 146 | do { 147 | return try operand.call("+@") 148 | } catch { 149 | fatalError("Calling '+@' on \(operand) failed: \(error)") 150 | } 151 | } 152 | } 153 | 154 | // MARK: - Subscript 155 | 156 | extension RbObject { 157 | /// Subscript operator, supports both get + set. 158 | /// 159 | /// Although you can use `RbObjectConvertible`s as the subscript arguments, 160 | /// the value assigned in the setter has to be an `RbObject`. So this doesn't 161 | /// work: 162 | /// ```swift 163 | /// try myObj[1, "fish", myThirdParamObj] = 4 164 | /// ``` 165 | /// ...instead you have to do: 166 | /// ```swift 167 | /// try myObj[1, "fish", myThirdParamObj] = RbObject(4) 168 | /// ``` 169 | /// 170 | /// - note: Calls Ruby `[]` and `[]=` methods. Crashes the process (`fatalError`) 171 | /// if anything goes wrong - Swift can't throw from subscripts yet. 172 | public subscript(args: any RbObjectConvertible...) -> RbObject { 173 | get { 174 | do { 175 | return try call("[]", args: args) 176 | } catch { 177 | fatalError("RbObject[] failed: \(error)") 178 | } 179 | } 180 | set { 181 | let allArgs = args + [newValue.rubyObject] 182 | do { 183 | try call("[]=", args: allArgs) 184 | } catch { 185 | fatalError("RbObject.[]= failed: \(error)") 186 | } 187 | } 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbProc.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbProc.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | internal import RubyGatewayHelpers 8 | 9 | /// A Ruby Proc. 10 | /// 11 | /// Use this to create a Ruby Proc from a symbol or any Ruby object 12 | /// supporting `to_proc`. 13 | /// 14 | /// This is most useful when passing a block to a method: 15 | /// ```swift 16 | /// // Ruby: mapped = names.map(&:downcase) 17 | /// let mapped = names.call("map", block: RbProc(RbSymbol("downcase"))) 18 | /// ``` 19 | /// 20 | /// Use `RbObject.init(blockCall:)` to create a Ruby Proc from a 21 | /// Swift closure. 22 | /// 23 | /// If you want to pass Swift code to a method as a block then just call 24 | /// `RbObjectAccess.call(_:args:kwArgs:blockRetention:blockCall:)` directly, 25 | /// no need for either `RbProc` or `RbObject`. 26 | public struct RbProc: RbObjectConvertible { 27 | private let sourceObject: any RbObjectConvertible 28 | 29 | /// Initialize from something that can be turned into a Ruby object. 30 | public init(object: any RbObjectConvertible) { 31 | sourceObject = object 32 | } 33 | 34 | /// Try to initialize from a Ruby object. 35 | /// 36 | /// Succeeds if the object can be used as a Proc (has `to_proc`). 37 | public init?(_ value: RbObject) { 38 | guard let obj = try? value.call("respond_to?", args: ["to_proc"]), 39 | obj.isTruthy else { 40 | return nil 41 | } 42 | self.init(object: value) 43 | } 44 | 45 | /// A Ruby object for the Proc 46 | public var rubyObject: RbObject { 47 | let srcObj = sourceObject.rubyObject 48 | guard Ruby.softSetup(), 49 | let procObj = try? srcObj.call("to_proc") else { 50 | return .nilObject 51 | } 52 | return procObj 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbRational.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbRational.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | /// A simple interface to Ruby's rational number support. 9 | /// 10 | /// This is not a Swift rational number library. It could be used 11 | /// as an interface between one such and Ruby. 12 | /// 13 | /// Ruby represents rational numbers internally as a positive or negative 14 | /// integer numerator and a positive integer denominator. Instances of this 15 | /// `RbRational` type converted from Ruby objects follow these rules; instances 16 | /// produced by Swift code using the `RbRational.init(numerator:denominator:)` 17 | /// method may not. 18 | /// 19 | /// ```swift 20 | /// let myRat = RbRational(numerator: myFractionTop, denominator: myFractionBot) 21 | /// 22 | /// let resultObj = myRubyService.call("addFinalSample", args: [myRat]) 23 | /// 24 | /// let myRatResult = RbRational(resultObj) 25 | /// ``` 26 | public struct RbRational: RbObjectConvertible { 27 | /// The rational number's numerator. 28 | public let numerator: Double 29 | /// The rational number's denominator. 30 | public let denominator: Double 31 | 32 | /// Create a new rational number. The parameters are normalized to give 33 | /// a positive denominator. 34 | public init(numerator: Double, denominator: Double) { 35 | if denominator < 0 { 36 | self.numerator = -numerator 37 | self.denominator = -denominator 38 | } else { 39 | self.numerator = numerator 40 | self.denominator = denominator 41 | } 42 | } 43 | 44 | /// Create a rational number from a Ruby object. 45 | /// 46 | /// This calls `#to_r` before extracting the parts so can 47 | /// be passed various types of Ruby object. 48 | /// 49 | /// Returns `nil` if the object cannot be converted or if its fractional 50 | /// parts parts cannot be converted to Swift `Double`s. 51 | /// See `RbError.history` to see why a conversion failed. 52 | public init?(_ value: RbObject) { 53 | guard let rat_obj = try? value.call("to_r"), 54 | let num_obj = try? rat_obj.call("numerator"), 55 | let denom_obj = try? rat_obj.call("denominator"), 56 | let num = Double(num_obj), 57 | let denom = Double(denom_obj) else { 58 | return nil 59 | } 60 | self.numerator = num 61 | self.denominator = denom 62 | } 63 | 64 | /// Convert some Swift data type to a rational. 65 | /// 66 | /// This is a convenience wrapper that lets you access Ruby's 67 | /// rational library directly from Swift types, for example: 68 | /// ```swift 69 | /// let rat = RbRational(0.3) 70 | /// ``` 71 | public init?(_ value: any RbObjectConvertible) { 72 | self.init(value.rubyObject) 73 | } 74 | 75 | /// Get a Ruby version of an `RbRational`. 76 | /// 77 | /// This can theoretically produce `RbObject.nilObject` if the environment 78 | /// has been nobbled in some way. 79 | public var rubyObject: RbObject { 80 | guard Ruby.softSetup(), 81 | let ratObject = try? Ruby.call("Rational", args: [numerator, denominator]) else { 82 | return .nilObject 83 | } 84 | return ratObject 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbSymbol.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbSymbol.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | internal import CRuby 8 | 9 | /// Represent a Ruby symbol. 10 | /// 11 | /// Ruby symbols are written `:name`. If in Ruby you would write: 12 | /// ```ruby 13 | /// obj.meth(:value) 14 | /// ``` 15 | /// 16 | /// Then the RubyGateway version is: 17 | /// ```swift 18 | /// try obj.call("meth", args: [RbSymbol("value")]) 19 | /// ``` 20 | public struct RbSymbol: RbObjectConvertible, Hashable { 21 | private let name: String 22 | 23 | /// Create from the name for the symbol. No leading colon. 24 | public init(_ name: String) { 25 | self.name = name 26 | } 27 | 28 | /// Try to create an `RbSymbol` from an `RbObject`. 29 | /// 30 | /// Always fails - no use for this, just use the `RbObject`. 31 | /// :nodoc: 32 | public init?(_ value: RbObject) { 33 | nil 34 | } 35 | 36 | /// A Ruby object for the symbol 37 | public var rubyObject: RbObject { 38 | guard Ruby.softSetup(), 39 | let id = try? Ruby.getID(for: name) else { 40 | return .nilObject 41 | } 42 | return RbObject(rubyValue: rb_id2sym(id)) 43 | } 44 | } 45 | 46 | // MARK: - CustomStringConvertible 47 | 48 | extension RbSymbol: CustomStringConvertible { 49 | /// A textual representation of the `RbSymbol` 50 | public var description: String { 51 | "RbSymbol(\(name))" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbThread.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbThread.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | internal import CRuby 8 | internal import RubyGatewayHelpers 9 | 10 | /// Context passed to thread callbacks wrapping up a Swift closure. 11 | internal final class RbThreadContext { 12 | let callback: () -> Void 13 | 14 | init(_ callback: @escaping () -> Void) { 15 | self.callback = callback 16 | } 17 | 18 | /// Call a function passing it a `void *` representation of the `RbThreadContext` 19 | func withRaw(rawCallback: (UnsafeMutableRawPointer) -> Void) { 20 | let unmanaged = Unmanaged.passRetained(self) 21 | defer { unmanaged.release() } 22 | rawCallback(unmanaged.toOpaque()) 23 | } 24 | 25 | /// Retrieve an `RbThreadContext` from its `void *` representation 26 | static func from(raw: UnsafeMutableRawPointer) -> RbThreadContext { 27 | Unmanaged.fromOpaque(raw).takeUnretainedValue() 28 | } 29 | } 30 | 31 | private func rbthread_callback(rawContext: UnsafeMutableRawPointer?) -> UnsafeMutableRawPointer? { 32 | let context = RbThreadContext.from(raw: rawContext!) 33 | context.callback() 34 | return nil 35 | } 36 | 37 | private func rbthread_ubf_callback(rawContext: UnsafeMutableRawPointer?) -> Void { 38 | let context = RbThreadContext.from(raw: rawContext!) 39 | context.callback() 40 | } 41 | 42 | /// This type provides a namespace for working with Ruby threads. 43 | /// 44 | /// You cannot call Ruby on arbitrary threads: only the very first thread 45 | /// where RubyGateway gets used or threads created by Ruby's `Thread` class. 46 | /// 47 | /// There is no way to 'attach' the Ruby runtime to a thread created by client 48 | /// code (eg. one accessed via libdispatch). 49 | /// 50 | /// Even when multiple Ruby threads are active, the VM executes just one at a 51 | /// time under control of a single lock known as the GVL. The GVL is given up 52 | /// over Ruby blocking operations and can be manually relinquished using 53 | /// `RbThread.callWithoutGvl(callback:)`. 54 | /// 55 | /// Inside a block that has given up the GVL, you must not call any Ruby code or 56 | /// it will at best crash. You can execute some code inside your GVL-free scope 57 | /// that is allowed to call Ruby using `RbThread.callWithGvl(callback:)`. 58 | public enum RbThread { 59 | /// Create a Ruby thread. 60 | /// 61 | /// This is a simple wrapper around creating a Ruby `Thread` object. 62 | /// 63 | /// - note: You must retain the returned `RbObject` until the Ruby thread 64 | /// has finished to ensure the Swift callback is not released 65 | /// prematurely. 66 | /// - parameter callback: Callback to make on the new thread 67 | /// - returns: The Ruby `Thread` object, or `nil` if there was a problem. 68 | /// See `RbError.history` for details of any error. 69 | public static func create(callback: @escaping @Sendable () -> Void) -> RbObject? { 70 | RbObject(ofClass: "Thread", retainBlock: true) { args in 71 | callback() 72 | return .nilObject 73 | } 74 | } 75 | 76 | /// Does the Ruby VM know about the current thread? 77 | /// 78 | /// - returns: `true` if this is the main thread or another created by Ruby 79 | /// where it's OK to call Ruby functions. 80 | public static func isRubyThread() -> Bool { 81 | ruby_native_thread_p() != 0 82 | } 83 | 84 | /// From a Ruby thread, run some non-Ruby code without the GVL. 85 | /// 86 | /// This allows other Ruby threads to run. See the Ruby source code 87 | /// for lengthy comments about how to do this safely. 88 | /// 89 | /// Using this API ends up with no unblocking function for the section. 90 | /// See `callWithoutGvl(unblocking:callback:)` to configure that. 91 | public static func callWithoutGvl(callback: () -> Void) { 92 | withoutActuallyEscaping(callback) { escapingCallback in 93 | let context = RbThreadContext(escapingCallback) 94 | context.withRaw { rawContext in 95 | rb_thread_call_without_gvl(rbthread_callback, rawContext, nil, nil) 96 | } 97 | } 98 | } 99 | 100 | /// A way to unblock a thread executing inside a `callWithoutGvl` section. 101 | public enum UnblockingFunc { 102 | /// Same as `RUBY_UBF_IO` 103 | /// 104 | /// For pthread platforms, sends `SIGVTALRM` to the thread until it wakes up. 105 | case io 106 | 107 | /// A custom unblocking function. 108 | /// 109 | /// See Ruby thread.c if in any doubt. 110 | case custom(() -> Void) 111 | } 112 | 113 | /// From a Ruby thread, run some non-Ruby code without the GVL. 114 | /// 115 | /// This allows other Ruby threads to run. See the Ruby source code 116 | /// for lengthy comments about how to do this safely. 117 | /// 118 | /// This version of the API takes an unblocking function to be used when 119 | /// Ruby wants to interrupt the thread and get it back under GVL control. 120 | public static func callWithoutGvl(unblocking: UnblockingFunc, callback: () -> Void) { 121 | withoutActuallyEscaping(callback) { escapingCallback in 122 | let context = RbThreadContext(escapingCallback) 123 | context.withRaw { rawContext in 124 | switch unblocking { 125 | case .custom(let ubfFunc): 126 | withoutActuallyEscaping(ubfFunc) { escapingUbfFunc in 127 | let ubfContext = RbThreadContext(escapingUbfFunc) 128 | ubfContext.withRaw { rawUbfContext in 129 | rb_thread_call_without_gvl(rbthread_callback, 130 | rawContext, 131 | rbthread_ubf_callback, 132 | rawUbfContext) 133 | } 134 | } 135 | case .io: 136 | rb_thread_call_without_gvl(rbthread_callback, rawContext, rbg_RUBY_UBF_IO(), nil) 137 | } 138 | } 139 | } 140 | } 141 | 142 | /// From a GVL-free section of code on a Ruby thread, reacquire the GVL and run some code. 143 | /// 144 | /// This cannot be used to attach a native thread to Ruby. It should only be used 145 | /// from within the `callback` passed to `callWithoutGvl(callback:)`. See the Ruby 146 | /// source code for more commentary. 147 | public static func callWithGvl(callback: () -> Void) { 148 | withoutActuallyEscaping(callback) { escapingCallback in 149 | let context = RbThreadContext(escapingCallback) 150 | context.withRaw { rawContext in 151 | rb_thread_call_with_gvl(rbthread_callback, rawContext) 152 | } 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /Sources/RubyGateway/RbVM.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RbVM.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | 7 | @preconcurrency internal import CRuby 8 | internal import RubyGatewayHelpers 9 | 10 | /// This class handles the setup and cleanup lifecycle events for the Ruby VM as well 11 | /// as storing data associated with the Ruby runtime. 12 | /// 13 | /// There can only be one of these for a process which is enforced by this class not 14 | /// being public + `RbGateway` holding the only instance. 15 | final class RbVM : @unchecked Sendable { 16 | /// State of Ruby lifecycle 17 | private enum State { 18 | /// Never tried 19 | case unknown 20 | /// Tried to set up, failed with something 21 | case setupError(Error) 22 | /// Set up OK 23 | case setup 24 | /// Cleaned up, can't be used 25 | case cleanedUp 26 | } 27 | /// Current state of the VM 28 | private var state: State 29 | 30 | /// Cache of rb_intern() calls. 31 | private var idCache: [String: ID] 32 | 33 | /// Protect state (bit pointless given Ruby's state but feels bad not to) 34 | private var lock: Lock 35 | 36 | /// Set up data 37 | init() { 38 | state = .unknown 39 | idCache = [:] 40 | // Paranoid about reentrant symbol lookup during finalizers... 41 | lock = Lock(recursive: true) 42 | } 43 | 44 | /// Check the state of the VM, make it better if possible. 45 | /// Returning means Ruby is working; throwing something means it is not. 46 | /// - returns: `true` on the actual setup, `false` subsequently. 47 | func setup() throws -> Bool { 48 | try lock.locked { 49 | switch state { 50 | case .setupError(let error): 51 | throw error 52 | case .setup: 53 | return false 54 | case .cleanedUp: 55 | try RbError.raise(error: .setup("Ruby has already been cleaned up.")) 56 | case .unknown: 57 | break 58 | } 59 | 60 | do { 61 | try doSetup() 62 | state = .setup 63 | } catch { 64 | state = .setupError(error) 65 | throw error 66 | } 67 | return true 68 | } 69 | } 70 | 71 | /// Shut down the Ruby VM and release resources. 72 | /// 73 | /// - returns: 0 if all is well, otherwise some error code. 74 | @discardableResult 75 | func cleanup() -> Int32 { 76 | lock.locked { 77 | guard case .setup = state else { 78 | return 0; 79 | } 80 | defer { state = .cleanedUp } 81 | return ruby_cleanup(0) 82 | } 83 | } 84 | 85 | /// Has Ruby ever been set up in this process? 86 | private var setupEver: Bool { 87 | rb_mKernel != 0 88 | } 89 | 90 | /// Initialize the Ruby VM for this process. The VM resources are freed up by `RbVM.cleanup()` 91 | /// or when there are no more refs to the `RbVM` object. 92 | /// 93 | /// There can only be one VM for a process. This means that you cannot create a second `RbVM` 94 | /// instance, even if the first instance has been cleaned up. 95 | /// 96 | /// The loadpath (where `require` looks) is set to the `lib/ruby` directories adjacent to the 97 | /// `libruby` the program is linked against and `$RUBYLIB`. Gems are enabled. 98 | /// 99 | /// - throws: `RbError.initError` if there is a problem starting Ruby. 100 | private func doSetup() throws { 101 | guard !setupEver else { 102 | try RbError.raise(error: .setup("Has already been done (via C API?) for this process.")) 103 | } 104 | 105 | // What is going on with init_stack 106 | // -------------------------------- 107 | // Line added for Ruby 3.4 because of ruby/ruby:9505 that took it out of `ruby_setup()`. 108 | // 109 | // This stack frame we're in now isn't very interesting except for defining the native thread 110 | // that will become the Ruby "main thread". Here is why it's OK to call this macro: 111 | // 112 | // `RUBY_INIT_STACK` declares a ‘stack’ variable and calls `vm.c:ruby_init_stack()` which 113 | // stores that variable address in `native_main_thread_stack_top`, the only place that is set. 114 | // 115 | // `eval.c:ruby_setup()` -> `vm.c:Init_BareVM()` is the only place that refers to the static 116 | // and passes to `thread.c:ruby_thread_init_stack()` for the current thread, which for platforms 117 | // we care about[1] leads to `thread_pthread.c:native_thread_init_stack()`. We only care about the 118 | // “main thread” use-case at this point and go to`thread_pthread.c:native_thread_init_main_thread_stack()`. 119 | // We only care about `MAINSTACKADDR_AVAILABLE` and so do not use the address to figure the 120 | // stack layout for GC. Then there is a sanity check which is the only place the address is 121 | // used - as long as it is within the stack as reported by pthreads then we are good. 122 | // 123 | // [1] Brief eyeball the win32 version looks OK too. 124 | // 125 | rbg_RUBY_INIT_STACK() 126 | 127 | let setup_rc = ruby_setup() 128 | guard setup_rc == 0 else { 129 | try RbError.raise(error: .setup("ruby_setup() failed: \(setup_rc)")) 130 | } 131 | 132 | // Calling ruby_options() sets up the loadpath nicely and does the bootstrapping of 133 | // rubygems so they can be required directly. 134 | // The -e part is to prevent it reading from stdin - empty script. 135 | let arg1 = strdup("RubyGateway") 136 | let arg2 = strdup("-e ") 137 | defer { 138 | arg1.map { free($0) } 139 | arg2.map { free($0) } 140 | } 141 | var argv = [arg1, arg2] 142 | let node = ruby_options(Int32(argv.count), &argv) 143 | 144 | var exit_status: Int32 = 0 145 | let node_status = ruby_executable_node(node, &exit_status) 146 | // `node` is a compiled version of the empty Ruby program. Which we, er, leak. Ahem. 147 | // `node_status` should be TRUE (NOT Qtrue!) because `node` is a program and not an error code. 148 | // `exit_status` should be 0 because it should be unmodified given `node` is a program. 149 | guard node_status == 1 && exit_status == 0 else { 150 | ruby_cleanup(0) 151 | try RbError.raise(error: .setup("ruby_executable_node() gave node_status \(node_status) exit status \(exit_status)")) 152 | } 153 | } 154 | 155 | /// Test hook to fake out 'setup error' state. 156 | func utSetSetupError() { 157 | let error = RbError.setup("Unit test setup failure") 158 | RbError.history.record(error: error) 159 | state = .setupError(error) 160 | } 161 | 162 | /// Test hook to fake out 'cleaned up' state. 163 | func utSetCleanedUp() { 164 | state = .cleanedUp 165 | } 166 | 167 | /// Test hook to get back to normal. 168 | func utSetSetup() { 169 | state = .setup 170 | } 171 | 172 | /// Get an `ID` ready to call a method, for example. 173 | /// 174 | /// Cache this on the Swift side. 175 | /// 176 | /// - parameter name: name to look up, typically constant or method name. 177 | /// - returns: the corresponding ID 178 | /// - throws: `RbException` if Ruby raises -- probably means the `ID` space 179 | /// is full, which is fairly unlikely. 180 | func getID(for name: String) throws -> ID { 181 | try lock.locked { 182 | if let rbId = idCache[name] { 183 | return rbId 184 | } 185 | let rbId = try RbVM.doProtect { tag in 186 | rbg_intern_protect(name, &tag) 187 | } 188 | idCache[name] = rbId 189 | return rbId 190 | } 191 | } 192 | 193 | /// Helper to call a protected Ruby API function and propagate any Ruby exception 194 | /// or unusual flow control as a Swift `RbException`. 195 | static func doProtect(call: (inout Int32) -> T) throws -> T { 196 | var tag = Int32(0) 197 | let result = call(&tag) 198 | 199 | let errorObj = RbObject(rubyValue: rb_errinfo()) 200 | guard !errorObj.isNil else { 201 | return result 202 | } 203 | 204 | switch errorObj.rubyType { 205 | case .T_OBJECT: 206 | // Normal case, a Ruby exception 207 | rb_set_errinfo(Qnil) 208 | try RbError.raise(error: .rubyException(RbException(exception: errorObj))) 209 | default: 210 | // Probably T_IMEMO for throw/break/return/etc. 211 | try RbError.raise(error: .rubyJump(tag)) 212 | } 213 | } 214 | } 215 | -------------------------------------------------------------------------------- /Sources/RubyGateway/String+RubyGateway.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+RubyGateway.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | 7 | // These hints are used to direct the 'smart' `RbInstanceAccess.get(...)` 8 | // method, rather than anything that relies on them being completely 9 | // correct - see true story in `rb_enc_symname_type()`. 10 | 11 | internal import CRuby 12 | 13 | extension String { 14 | /// Helper to test type and throw if wrong 15 | private func check(_ predPath: KeyPath, _ type: String) throws { 16 | guard self[keyPath: predPath] else { 17 | throw RbError.badIdentifier(type: type, id: self) 18 | } 19 | } 20 | 21 | /// Does the string look like a Ruby constant name? 22 | var isRubyConstantName: Bool { 23 | guard !contains("::") else { 24 | return false 25 | } 26 | // Ruby supports full utf8 character set for identifiers. 27 | // However Ruby constants are defined as beginning with an ASCII 28 | // capital letter. `rb_isupper` is locale-insensitive. 29 | if let firstChar = utf8.first { 30 | return rb_isupper(Int32(firstChar)) != 0 31 | } 32 | return false 33 | } 34 | 35 | /// Throw if the string does not look like a constant name. 36 | func checkRubyConstantName() throws { 37 | try check(\String.isRubyConstantName, "constant (capital, no ::)") 38 | } 39 | 40 | /// Does the string look like a Ruby constant path name? 41 | var isRubyConstantPath: Bool { 42 | guard !hasSuffix("::") else { 43 | return false 44 | } 45 | return components(separatedBy: "::").allSatisfy { $0.isRubyConstantName } 46 | } 47 | 48 | /// Throw if the string does not look like a constant path. 49 | func checkRubyConstantPath() throws { 50 | try check(\String.isRubyConstantPath, "constant (capital)") 51 | } 52 | 53 | /// Does the string look like a Ruby global variable name? 54 | var isRubyGlobalVarName: Bool { 55 | starts(with: "$") 56 | } 57 | 58 | /// Throw if the string does not look like a global variable name. 59 | func checkRubyGlobalVarName() throws { 60 | try check(\String.isRubyGlobalVarName, "global var ($)") 61 | } 62 | 63 | /// Does the string look like a Ruby instance variable name? 64 | var isRubyInstanceVarName: Bool { 65 | starts(with: "@") && !isRubyClassVarName 66 | } 67 | 68 | /// Throw if the string does not look like an instance var name. 69 | func checkRubyInstanceVarName() throws { 70 | try check(\String.isRubyInstanceVarName, "instance var (@)") 71 | } 72 | 73 | /// Does the string look like a Ruby class variable name? 74 | var isRubyClassVarName: Bool { 75 | starts(with: "@@") 76 | } 77 | 78 | /// Throw if the string does not look like a class var name. 79 | func checkRubyClassVarName() throws { 80 | try check(\String.isRubyClassVarName, "class var (@@)") 81 | } 82 | 83 | /// Does the string look like a Ruby method name? 84 | var isRubyMethodName: Bool { 85 | !isRubyConstantName && !isRubyGlobalVarName && !isRubyInstanceVarName && !isRubyClassVarName 86 | } 87 | 88 | /// Throw if the string does not look like a method name. 89 | func checkRubyMethodName() throws { 90 | try check(\String.isRubyMethodName, "method") 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /Sources/RubyGatewayHelpers/include/module.modulemap: -------------------------------------------------------------------------------- 1 | module RubyGatewayHelpers { 2 | header "rbg_helpers.h" 3 | export * 4 | } 5 | -------------------------------------------------------------------------------- /Sources/RubyGatewayHelpers/rbg_macros.m: -------------------------------------------------------------------------------- 1 | // 2 | // rbg_macros.m 3 | // RubyGatewayHelpers 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | @import CRuby; 9 | #import "rbg_helpers.h" 10 | 11 | // The RSTRING routines accesss the underlying structures 12 | // that have too many unions for Swift to access safely. 13 | // 14 | // And they go from macros to inlines in Ruby 3 which the 15 | // Swift importer sort-of imports. 16 | long rbg_RSTRING_LEN(VALUE v) 17 | { 18 | return RSTRING_LEN(v); 19 | } 20 | 21 | const char *rbg_RSTRING_PTR(VALUE v) 22 | { 23 | return RSTRING_PTR(v); 24 | } 25 | 26 | // # Version constants 27 | // These are exported as char [] which don't get imported 28 | const char *rbg_ruby_version(void) 29 | { 30 | return ruby_version; 31 | } 32 | 33 | const char *rbg_ruby_description(void) 34 | { 35 | return ruby_description; 36 | } 37 | 38 | // This is '-1' cast to pfn... 39 | rb_unblock_function_t * _Nonnull rbg_RUBY_UBF_IO(void) 40 | { 41 | return RUBY_UBF_IO; 42 | } 43 | 44 | // Ruby pre-3 and 3+ 45 | 46 | // Ruby 3 adds actual C enums for ruby_value_type and ruby_special_constants. 47 | // These import into Swift as different types so we collapse here. 48 | int rbg_type(VALUE v) { return rb_type(v); } 49 | int rbg_qfalse(void) { return RUBY_Qfalse; } 50 | int rbg_qtrue(void) { return RUBY_Qtrue; } 51 | int rbg_qnil(void) { return RUBY_Qnil; } 52 | int rbg_qundef(void) { return RUBY_Qundef; } 53 | 54 | // These become inlines in Ruby 3 that get imported 55 | int rbg_RB_TEST(VALUE v) { return RB_TEST(v); } 56 | int rbg_RB_NIL_P(VALUE v) { return RB_NIL_P(v); } 57 | 58 | // See comment on call in RbVM.swift 59 | void rbg_RUBY_INIT_STACK(void) { 60 | RUBY_INIT_STACK; 61 | } 62 | -------------------------------------------------------------------------------- /Sources/RubyGatewayHelpers/rbg_value.m: -------------------------------------------------------------------------------- 1 | // 2 | // rbg_value.m 3 | // RubyGatewayHelpers 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | @import CRuby; 9 | #import "rbg_helpers.h" 10 | 11 | // Fixups for Ruby < 2.3 12 | 13 | #ifndef RB_SPECIAL_CONST_P 14 | #define RB_SPECIAL_CONST_P SPECIAL_CONST_P 15 | #endif 16 | 17 | // 18 | // # VALUE protection. 19 | // 20 | // Ruby GC relies on being able to find VALUEs that are in use. 21 | // Most of the C world relies on these being on the stack, which Ruby snoops. 22 | // Approach here to is to store each VALUE in a known-address box associated 23 | // with each Swift `RbObject`. Then Ruby APIs are used to register this special 24 | // address with the GC, tied to the lifetime of the `RbObject`. 25 | // 26 | // The Ruby `rb_gc_register_address` APIs are not super-scalable (SLL) but should 27 | // be OK for our use cases. If it turns out to be too slow then we will need to 28 | // implement a single parent Ruby object registered with GC and treat all of the 29 | // `RbObject`s as efficiently stored dynamic children, participating in the GC 30 | // protocols as required. Sounds fun, might do that anyway! 31 | // 32 | 33 | Rbg_value * _Nonnull rbg_value_alloc(VALUE value) 34 | { 35 | Rbg_value *box = malloc(sizeof(*box)); 36 | if (box == NULL) 37 | { 38 | // No good way out here, don't want to make the RbEnv 39 | // initializers failable. 40 | abort(); 41 | } 42 | box->value = value; 43 | 44 | // Subtlety - it would do no harm to register constants except that 45 | // in the scenario where Ruby is not functioning we use Qnil etc. instead 46 | // of actual values to avoid crashing, and we mustn't talk to the GC... 47 | if (!RB_SPECIAL_CONST_P(value)) 48 | { 49 | rb_gc_register_address(&box->value); 50 | } 51 | return box; 52 | } 53 | 54 | Rbg_value *rbg_value_dup(const Rbg_value * _Nonnull box) 55 | { 56 | return rbg_value_alloc(box->value); 57 | } 58 | 59 | void rbg_value_free(Rbg_value * _Nonnull box) 60 | { 61 | if (!RB_SPECIAL_CONST_P(box->value)) 62 | { 63 | rb_gc_unregister_address(&box->value); 64 | } 65 | box->value = Qundef; 66 | free(box); 67 | } 68 | -------------------------------------------------------------------------------- /Sources/RubyThreadSample/RubyExecutor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // RubyExecutor.swift 3 | // RubyThreadSample 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import Foundation 9 | import RubyGateway 10 | 11 | /// A serial executor bound to its own thread. 12 | /// 13 | /// Initializes Ruby on the thread and shuts it down (``RubyGateway.cleanup``) if the executor is stopped. 14 | @available(macOS 14, *) 15 | final class RubyExecutor: SerialExecutor, @unchecked Sendable { 16 | /// Combination mutex & CV protecting ``jobs`` and ``quit`` and ``thread`` 17 | private let cond: NSCondition 18 | 19 | /// Swift concurrency work pending execution 20 | private var jobs: [UnownedJob] 21 | 22 | /// Interlocked state for ``stop()`. 23 | private enum Quit { 24 | case no 25 | case sent 26 | case done 27 | } 28 | private var quit: Quit 29 | 30 | // MARK: Lifecycle 31 | 32 | /// Create a new dedicated-thread executor 33 | /// 34 | /// - Parameters: 35 | /// - qos: The ``QualityOfService`` for the executor's thread. 36 | /// - name: A name for the executor's thread for debug. 37 | public init(qos: QualityOfService = .default, name: String = "RubyExecutor") { 38 | self.cond = NSCondition() 39 | self.jobs = [] 40 | self.quit = .no 41 | self.qos = qos 42 | self.name = name 43 | self.cond.name = "\(name) CV" 44 | self._thread = nil 45 | 46 | Thread.detachNewThread { [unowned self] in 47 | Thread.current.qualityOfService = qos 48 | Thread.current.name = name 49 | thread = Thread.current 50 | threadMain() 51 | thread = nil 52 | } 53 | } 54 | 55 | /// Stop the executor. 56 | /// 57 | /// Blocks until the thread has finished any pending jobs and cleaned up Ruby. 58 | /// If any actors still exist associated with this then they will stop working in a bad way. 59 | /// 60 | /// It's not at all mandatory to call this - only if you are relying on Ruby's "graceful shutdown" 61 | /// path for some reason. 62 | public func stop() { 63 | cond.withLock { 64 | guard quit == .no else { 65 | return 66 | } 67 | quit = .sent 68 | cond.signal() 69 | 70 | while quit == .sent { 71 | cond.wait() 72 | } 73 | } 74 | } 75 | 76 | // MARK: Properties 77 | 78 | /// The `QualityOfService`used by the executor's thread 79 | public let qos: QualityOfService 80 | 81 | /// The (debug) name associated with the executor's thread and locks 82 | public let name: String 83 | 84 | private var _thread: Thread? 85 | 86 | /// The ``Thread`` for the executor, or ``nil`` if it's not running 87 | public private(set) var thread: Thread? { 88 | get { 89 | cond.withLock { _thread } 90 | } 91 | set { 92 | cond.withLock { _thread = newValue } 93 | } 94 | } 95 | 96 | private func threadMain() { 97 | _ = Ruby.softSetup() 98 | 99 | cond.lock() 100 | 101 | while quit == .no { 102 | if jobs.isEmpty { 103 | cond.wait() 104 | } 105 | 106 | let loopJobs = jobs 107 | jobs = [] 108 | 109 | cond.unlock() 110 | 111 | for job in loopJobs { 112 | job.runSynchronously(on: asUnownedSerialExecutor()) 113 | } 114 | 115 | cond.lock() 116 | } 117 | 118 | cond.unlock() 119 | _ = Ruby.cleanup() 120 | cond.lock() 121 | 122 | quit = .done 123 | cond.signal() 124 | 125 | cond.unlock() 126 | } 127 | 128 | /// Send a job to be executed later on the thread 129 | /// 130 | /// Called by the Swift runtime, do not call. :nodoc: 131 | public func enqueue(_ job: consuming ExecutorJob) { 132 | let unownedJob = UnownedJob(job) 133 | cond.withLock { 134 | jobs.append(unownedJob) 135 | cond.signal() 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Sources/RubyThreadSample/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // RubyThreadSample 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | import RubyGateway 8 | 9 | @available(macOS 14, *) 10 | actor RubyActor { 11 | nonisolated let unownedExecutor: UnownedSerialExecutor 12 | init(executor: RubyExecutor) { 13 | self.unownedExecutor = executor.asUnownedSerialExecutor() 14 | } 15 | 16 | func rand() async throws -> String { 17 | let ver = Ruby.version 18 | let result = try Ruby.eval(ruby: "Kernel.rand") 19 | return "Ruby (\(ver)) random: \(result)" 20 | } 21 | } 22 | 23 | @MainActor 24 | @available(macOS 14, *) 25 | func doRubyWork() async { 26 | do { 27 | let executor = RubyExecutor() 28 | let actor = RubyActor(executor: executor) 29 | let result = try await actor.rand() 30 | print(result) 31 | // This is optional - fine to just let the process exit. 32 | executor.stop() 33 | } catch { 34 | print("error: \(error)") 35 | } 36 | } 37 | 38 | if #available(macOS 14, *) { 39 | await doRubyWork() 40 | } 41 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/backwards.rb: -------------------------------------------------------------------------------- 1 | def backwards(arg) 2 | arg.reverse! 3 | end 4 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/demo.rb: -------------------------------------------------------------------------------- 1 | # demonstrator 2 | module Academy 3 | class Student 4 | 5 | class SubjectScores 6 | attr_reader :count, :total 7 | 8 | def initialize(count, total) 9 | @count = count 10 | @total = total 11 | end 12 | 13 | def add(score) 14 | @count += 1 15 | @total += score 16 | end 17 | 18 | def mean 19 | Float(total)/Float(count) 20 | end 21 | 22 | def to_s 23 | "#{count} scores, mean #{mean}" 24 | end 25 | end 26 | 27 | attr_reader :name, :scores 28 | 29 | def add_score(subject, score) 30 | current = scores[subject] 31 | if current 32 | current.add(score) 33 | else 34 | scores[subject] = SubjectScores.new(1, score) 35 | end 36 | end 37 | 38 | def score_for_subject(subject) 39 | scores[subject] or nil 40 | end 41 | 42 | def mean_score_for_subject(subject) 43 | s = score_for_subject(subject) 44 | return nil unless s 45 | s.mean 46 | end 47 | 48 | def initialize(name:) 49 | @name = name 50 | @scores = Hash.new 51 | end 52 | 53 | def to_s 54 | "#{name}: #{scores.to_s}" 55 | end 56 | end 57 | 58 | class YearGroup 59 | attr_reader :students, :subjects 60 | 61 | def initialize(subjects = [:reading, :riting, :rithmetic]) 62 | @students = [] 63 | @subjects = subjects 64 | end 65 | 66 | def add_student(student) 67 | students.push(student) #Student.new(name: name)) 68 | end 69 | 70 | def run_test(subject) 71 | raise "Bad subject #{subject}" unless @subjects.include?(subject) 72 | raise "need block" unless block_given? 73 | 74 | students.each do |student| 75 | score = yield(student.name, subject) 76 | student.add_score(subject, score) 77 | end 78 | end 79 | 80 | def report 81 | puts("#{students.count} students:") 82 | students.each do |student| 83 | puts(" #{student.to_s}") 84 | end 85 | end 86 | end 87 | end 88 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/endtoend.rb: -------------------------------------------------------------------------------- 1 | module RubyGateway 2 | class EndToEnd 3 | attr_accessor :name 4 | attr_reader :version 5 | 6 | def initialize(version, name:) 7 | @version = version 8 | @name = name 9 | end 10 | 11 | def to_s 12 | "#{name} (version #{version})" 13 | end 14 | 15 | def give_name 16 | yield self.name 17 | end 18 | 19 | def always_raise 20 | raise "Always raising" 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/inspectables.rb: -------------------------------------------------------------------------------- 1 | class Uninspectable < BasicObject 2 | end 3 | 4 | class Inspectable 5 | attr_reader :attr1 6 | attr_accessor :attr2 7 | 8 | def initialize 9 | @attr1 = 23 10 | self.attr2 = ["one", "dozen", 3.4] 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/methods.rb: -------------------------------------------------------------------------------- 1 | class MethodsTest 2 | attr_accessor :property 3 | 4 | def initialize 5 | self.property = "Default" 6 | 7 | @@property = "ClassDefault" 8 | 9 | @doubleMethod = :double 10 | end 11 | 12 | def noArgsMethod 13 | # Don't return anything either 14 | end 15 | 16 | def threeArgsMethod(aString, aInt, aFloat) 17 | unless aString.class == String 18 | raise "Not a string: #{aString.class}" 19 | end 20 | unless aInt.class == Integer 21 | raise "Not a fixnum integer: #{aInt.class}" 22 | end 23 | unless aFloat.class == Float 24 | raise "Not a float: #{aFloat.class}" 25 | end 26 | "OK" 27 | end 28 | 29 | def self.classMethod 30 | true 31 | end 32 | 33 | def kwArgsMethod(aFirst, aSecond:, aThird: 1) 34 | aFirst + aSecond * aThird 35 | end 36 | 37 | def double(x) 38 | return x * 2 39 | end 40 | 41 | def expectsNil(arg) 42 | raise "It's not nil: #{arg}" unless arg.nil? 43 | end 44 | 45 | def store_block(&block) 46 | @stored_block = block 47 | end 48 | 49 | def call_block 50 | @stored_block.call() 51 | end 52 | 53 | def yielder(value:) 54 | raise "No block given" unless block_given? 55 | yield(value, "fish") 56 | end 57 | 58 | def yielder2 59 | raise "No block given 2" unless block_given? 60 | yield(22, "fish") 61 | end 62 | 63 | def yielder3(value) 64 | raise "No block given 3" unless block_given? 65 | yield a: value 66 | end 67 | 68 | def [](a, b) 69 | "#{a} #{b}" 70 | end 71 | 72 | def []=(a, b, new) 73 | @subscript_set = "#{a} #{b} = #{new}" 74 | end 75 | 76 | def get_num_array 77 | [1, 2, 3] 78 | end 79 | 80 | def sum_array(ary) 81 | sum = 0 82 | ary.each { |a| sum += a } 83 | sum 84 | end 85 | 86 | def get_sym_num_hash 87 | { a: 1, b: 2, c: 3} 88 | end 89 | 90 | def get_ambiguous_hash 91 | { 1 => "a", 1.0 => "b" } 92 | end 93 | 94 | def self.get_str_set 95 | Set.new(["one", "two", "three"]) 96 | end 97 | 98 | def self.get_ambiguous_num_set 99 | Set.new([1, 1.0]) 100 | end 101 | 102 | def to_a 103 | [1, "two", 3.0] 104 | end 105 | end 106 | 107 | class TestBlockClass 108 | attr_accessor :value 109 | 110 | def initialize 111 | self.value = yield 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/nesting.rb: -------------------------------------------------------------------------------- 1 | module Outer 2 | OUTER_CONSTANT = 1 3 | module Middle 4 | MIDDLE_CONSTANT = 2 5 | class Inner 6 | INNER_CONSTANT = 3 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/nonconvert.rb: -------------------------------------------------------------------------------- 1 | # class without to_* 2 | class Nonconvert < BasicObject 3 | end 4 | 5 | # class without to_str 6 | class JustToS 7 | def to_s 8 | "to_s" 9 | end 10 | end 11 | 12 | # class with to_str and to_s 13 | class BothToSAndToStr 14 | def to_s 15 | "to_s" 16 | end 17 | 18 | def to_str 19 | "to_str" 20 | end 21 | end 22 | 23 | # class with trap to_a 24 | class NotArrayable 25 | def to_a 26 | 1 27 | end 28 | end 29 | 30 | # class without to_h + to_hash 31 | class NotHashable 32 | end 33 | 34 | # class without to_hash 35 | class JustToH 36 | def to_h 37 | { 1 => 2} 38 | end 39 | end 40 | 41 | # class with both 42 | class BothToHAndToHash 43 | def to_hash 44 | { 1 => 2 } 45 | end 46 | 47 | def to_h 48 | { :bad => "news" } 49 | end 50 | end 51 | 52 | # class with trap to_hash 53 | class TrapToHash 54 | def to_hash 55 | "Not remotely a hash" 56 | end 57 | end 58 | 59 | # class that looks like Range but is bad 60 | class BadRange 61 | def begin 62 | 100 63 | end 64 | 65 | def end 66 | 2 67 | end 68 | 69 | def exclude_end? 70 | true 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/numbers.rb: -------------------------------------------------------------------------------- 1 | class TestNumbers 2 | BIG_NUM = 2 ** 80 3 | 4 | def to_int 5 | -4 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/raising.rb: -------------------------------------------------------------------------------- 1 | def raiseString 2 | raise "string" 3 | end 4 | 5 | def stackSmash 6 | stackSmash 7 | end 8 | 9 | def doExit 10 | exit 11 | end 12 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/swift_classes.rb: -------------------------------------------------------------------------------- 1 | # See TestClassDef.swift 2 | 3 | class MyParentClass 4 | def value 5 | 22 6 | end 7 | 8 | def other_value 9 | 44 10 | end 11 | end 12 | 13 | # Swift: 14 | # module MyOuterModule 15 | # module MyInnerModule 16 | # class MyClass < MyParentClass 17 | # def value 18 | # 100 19 | # end 20 | # end 21 | # end 22 | # end 23 | 24 | def test_swiftclass 25 | inst = MyOuterModule::MyInnerModule::MyClass.new 26 | val = inst.value 27 | raise "Bad val #{val}" unless val == 100 28 | 29 | valo = inst.other_value 30 | raise "Bad other_val #{valo}" unless valo == 44 31 | end 32 | 33 | module InjectableModule 34 | def value1 35 | 22 36 | end 37 | 38 | def value2 39 | 29 40 | end 41 | end 42 | 43 | class InjecteeClass1 44 | def value1 45 | 50 46 | end 47 | end 48 | 49 | class InjecteeClass2 50 | def value1 51 | 30 52 | end 53 | end 54 | 55 | # Swift: 56 | # class InjecteeClass1 57 | # include InjectableModule 58 | # end 59 | # 60 | # class InjecteeClass2 61 | # prepend InjectableModule 62 | # end 63 | 64 | def test_inject1 65 | o1 = InjecteeClass1.new 66 | v1 = o1.value1 67 | raise "Bad v1 #{v1}" unless v1 == 50 68 | v2 = o1.value2 69 | raise "Bad v2 #{v2}" unless v2 == 29 70 | 71 | o2 = InjecteeClass2.new 72 | v3 = o2.value1 73 | raise "Bad v3 #{v3}" unless v3 == 22 74 | v4 = o2.value2 75 | raise "Bad v4 #{v4}" unless v4 == 29 76 | end 77 | 78 | # Swift: 79 | # class InjecteeClass1 80 | # extend InjectableModule 81 | # end 82 | 83 | def test_inject2 84 | v1 = InjecteeClass1.value1 85 | raise "Bad 2.v1 #{v1}" unless v1 == 22 86 | end 87 | 88 | # Swift: 89 | # class PeerMethods 90 | # def fingerprint 91 | # "FINGERPRINT" 92 | # end 93 | # end 94 | 95 | def test_bound1 96 | i1 = PeerMethods.new 97 | v1 = i1.fingerprint 98 | raise "Bad fingerprint" unless v1 == "FINGERPRINT" 99 | i1 = 1 100 | end 101 | 102 | # Swift: 103 | # class Invader 104 | # def initialize(name) 105 | # end 106 | # 107 | # def fire 108 | # end 109 | # 110 | # def name 111 | # end 112 | # end 113 | 114 | def test_invader 115 | inv1 = Invader.new("fred") 116 | n = inv1.name 117 | raise "Bad name" unless n == "fred" 118 | stats = inv1.list_stats 119 | inv1.list_stats do |s_name, s_count| 120 | stats.delete_if { |s| s == s_name } 121 | stats.delete_if { |s| s == s_count } 122 | end 123 | raise "Bad stats? #{stats}" unless stats.empty? 124 | r = inv1.fire 125 | raise "Bad fire rc #{r}" unless r == inv1 126 | true 127 | end 128 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/swift_methods.rb: -------------------------------------------------------------------------------- 1 | # Testcases for passing args and things to Swift. 2 | # 3 | # See TestMethods.swift 4 | # 5 | 6 | # Sticky situations caused by invoking blocks from Swift code. 7 | # 8 | # Swift defines, basically: 9 | # 10 | # def swift_calls_block 11 | # yield 12 | # return 100 13 | # end 14 | # 15 | # def swift_returns_block 16 | # yield 17 | # end 18 | # 19 | # def swift_calls_block_kw(value) 20 | # yield(kwa: value) 21 | # end 22 | 23 | def ruby_should_return_100 24 | swift_calls_block { 42 } 25 | end 26 | 27 | def ruby_should_return_42 28 | swift_calls_block { break 42 } 29 | end 30 | 31 | def ruby_should_return_200 32 | swift_calls_block { break 42 } 33 | 200 34 | end 35 | 36 | def ruby_should_return_44 37 | swift_calls_block { return 44 } 38 | 200 39 | end 40 | 41 | def ruby_should_return_22 42 | swift_returns_block { 22 } 43 | end 44 | 45 | def ruby_should_return_24 46 | swift_returns_block do 47 | next 24 48 | 22 49 | end 50 | end 51 | 52 | def ruby_should_return_4 53 | rv = 0 54 | swift_calls_block { 55 | rv += 1 56 | redo if rv < 4 57 | } 58 | rv 59 | end 60 | 61 | def ruby_should_return_89 62 | swift_calls_block_kw(88) { |arg = 1, kwa:| arg + kwa } 63 | end 64 | 65 | # Keyword args from Ruby to Swift 66 | # 67 | # Swift defines: 68 | # 69 | # def swift_kwargs(a:, b:, c: 2, d: 3) 70 | # a + b + c + d 71 | # end 72 | 73 | def ruby_kw_should_return_9 74 | swift_kwargs(a: 3, b: 1) 75 | end 76 | 77 | def ruby_kw_should_return_20 78 | swift_kwargs(a: 3, b: 1, c: 13) 79 | end 80 | 81 | def ruby_kw_should_return_14 82 | swift_kwargs(a: 3, b: 1, c: 7, d: 3) 83 | end 84 | 85 | def ruby_kw_should_return_100 86 | swift_kwargs 87 | rescue 88 | 100 89 | end 90 | 91 | def ruby_kw_should_return_200 92 | swift_kwargs(e: 14) 93 | rescue 94 | 200 95 | end 96 | 97 | # Swift defines: 98 | # def log(msg); 99 | # def log2(message:, priority: 1); 100 | def ruby_test_logging_functions 101 | log("Log 1") 102 | log2(message: "Log 2") 103 | log2(message: "Log 3", priority: 2) 104 | end 105 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/swift_obj_methods.rb: -------------------------------------------------------------------------------- 1 | 2 | # Testcases for defining methods in Swift. 3 | # 4 | # See TestObjMethods.swift 5 | # 6 | 7 | class EmptyClass < Object 8 | end 9 | 10 | # swift: 11 | # 12 | # class EmptyClass 13 | # def double(val) 14 | # val * 2 15 | # end 16 | # end 17 | 18 | def test_simple 19 | e = EmptyClass.new 20 | two = e.double(1) 21 | raise "Wrong answer: #{two}" unless two == 2 22 | end 23 | 24 | module EmptyModule 25 | end 26 | 27 | class ClassFromEmptyModule < Object 28 | include EmptyModule 29 | end 30 | 31 | # swift: 32 | # 33 | # module EmptyModule 34 | # def answer 35 | # "true" 36 | # end 37 | # end 38 | 39 | def test_module 40 | c = ClassFromEmptyModule.new 41 | tr = c.answer 42 | raise "Wrong answer: #{tr}" unless tr == "true" 43 | end 44 | 45 | class IdentifiedClass < Object 46 | attr_accessor :uniqueId 47 | 48 | def initialize(newId) 49 | self.uniqueId = newId 50 | end 51 | end 52 | 53 | # swift: 54 | # 55 | # class IdentifiedClass 56 | # def doubleId 57 | # uniqueId * 2 58 | # end 59 | # end 60 | 61 | def test_self_access 62 | o1 = IdentifiedClass.new(13) 63 | o2 = IdentifiedClass.new(29) 64 | v1 = o1.doubleId 65 | raise "Wrong answer 1 #{v1}" unless v1 == 26 66 | v2 = o2.doubleId 67 | raise "Wrong answer 2 #{v2}" unless v2 == 58 68 | end 69 | 70 | class BaseClass < Object 71 | end 72 | 73 | class DerivedClass < BaseClass 74 | end 75 | 76 | # swift: 77 | # 78 | # class BaseClass 79 | # def getValue 80 | # 22 81 | # end 82 | # end 83 | 84 | def test_inherited 85 | o1 = BaseClass.new 86 | o2 = DerivedClass.new 87 | v1 = o1.getValue 88 | raise "Wrong answer base #{v1}" unless v1 == 22 89 | v2 = o2.getValue 90 | raise "Wrong answer derived #{v2}" unless v2 == 22 91 | end 92 | 93 | class OverriddenClass < Object 94 | def getValue 95 | 33 96 | end 97 | end 98 | 99 | # swift: 100 | # 101 | # class OverriddenClass 102 | # def getValue 103 | # 22 104 | # end 105 | # end 106 | 107 | def test_overridden 108 | o1 = OverriddenClass.new 109 | v1 = o1.getValue 110 | raise "Wrong answer 1 #{v1}" unless v1 == 22 111 | end 112 | 113 | class SingSimpleClass 114 | def answer 115 | 22 116 | end 117 | end 118 | 119 | # test is entirely in Swift 120 | 121 | class SingBase < Object 122 | end 123 | 124 | class SingDerived < SingBase 125 | end 126 | 127 | # swift: 128 | # 129 | # class SingBase 130 | # def self.value2 131 | # 10 132 | # end 133 | # end 134 | 135 | def test_ston_overridden 136 | v1 = SingDerived.value2 137 | raise "Bad value #{v1}" unless v1 == 10 138 | end 139 | 140 | class SuperBase 141 | def override_me 142 | 22 143 | end 144 | 145 | def override_me_too(a, b:) 146 | a + b 147 | end 148 | end 149 | 150 | # swift: 151 | # 152 | # class SuperDerived < SuperBase 153 | # def override_me 154 | # super 155 | # end 156 | # 157 | # def override_me_too 158 | # super(1, b: 4) 159 | # end 160 | # 161 | # def override_error 162 | # super 163 | # end 164 | # end 165 | 166 | def test_override_super 167 | val = SuperDerived.new 168 | o1 = val.override_me 169 | raise "Bad 1 value #{o1}" unless o1 == 22 170 | o2 = val.override_me_too 171 | raise "Bad 2 value #{o2}" unless o2 == 5 172 | true 173 | end 174 | 175 | def test_override_super2 176 | val = SuperDerived.new 177 | val.override_error 178 | end 179 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Fixtures/unloadable.rb: -------------------------------------------------------------------------------- 1 | this is not ruby 2 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/Helpers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // VMWrapper.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import Foundation 9 | import RubyGateway 10 | import XCTest 11 | 12 | extension XCTestCase { 13 | /// Standard wrapper for error checking 14 | func doErrorFree(call: () throws -> ()) { 15 | do { 16 | try call() 17 | } catch { 18 | XCTFail("Unexpected error: \(error)") 19 | } 20 | } 21 | 22 | /// Standard wrapper for error checking 23 | func doErrorFree(fallback: T, call: () throws -> T) -> T { 24 | do { 25 | return try call() 26 | } catch { 27 | XCTFail("Unexpected error: \(error)") 28 | return fallback 29 | } 30 | } 31 | 32 | /// Standard wrapper for expected errors 33 | func doError(call: () throws -> ()) { 34 | do { 35 | try call() 36 | // Shouldn't really get here, want more explicit fail in client 37 | XCTFail("No error thrown") 38 | } catch { 39 | print("Caught and swallowed \(error)") 40 | } 41 | } 42 | } 43 | 44 | /// Misc test helpers 45 | struct Helpers { 46 | /// Ruby's lifetime rules mean that we can't create + tear down VMs willy-nilly as 47 | /// one would naturally do in a test environment. So we set up a singleton here. 48 | /// 49 | /// To test that 'cleanup' actually works we will need a second test target (and hope 50 | /// that the test runner will treat that as a separate process.) 51 | 52 | /// Ruby files etc. 53 | private static let fixturesDir: String = { 54 | URL(fileURLWithPath: #filePath).deletingLastPathComponent().path + "/Fixtures" 55 | }() 56 | 57 | /// Get full path to fixture with name 58 | static func fixturePath(_ name: String) -> String { 59 | "\(fixturesDir)/\(name)" 60 | } 61 | 62 | /// A weird Swift type that has distinct Swift instances 63 | /// but identical Ruby instances. Simulate some kind of client bug... 64 | struct ImpreciseRuby: RbObjectConvertible, Hashable { 65 | let val: Int 66 | 67 | init(_ val: Int) { 68 | self.val = val 69 | } 70 | 71 | init?(_ value: RbObject) { 72 | nil 73 | } 74 | 75 | var rubyObject: RbObject { 76 | RbObject(42) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestArrays.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestArrays.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | class TestArrays: XCTestCase { 12 | 13 | // Kudos to Swift to be able to write this genericly... 14 | private func doTestRoundTrip(arr: [T]) where T: RbObjectConvertible, T: Equatable { 15 | let arrObj = RbObject(arr) 16 | for (offset, elt) in arr.enumerated() { 17 | XCTAssertEqual(elt, T(arrObj[offset])) 18 | } 19 | guard let arrBack = Array(arrObj) else { 20 | XCTFail("Couldn't get back to Swift array") 21 | return 22 | } 23 | XCTAssertEqual(arr, arrBack) 24 | } 25 | 26 | /// One primitive... 27 | func testRoundTripInt() { 28 | doTestRoundTrip(arr: [1, 2, 3]) 29 | } 30 | 31 | /// Another primitive... 32 | func testRoundTripString() { 33 | doTestRoundTrip(arr: ["one", "two", "three"]) 34 | } 35 | 36 | /// Arrays of arrays. Can't mix types using this kind of interface. 37 | func testRoundTripNested() { 38 | doTestRoundTrip(arr: [ ["a", "b", "c"], ["x", "y"], ["q"] ]) 39 | } 40 | 41 | /// Ruby understands our arrays + vice versa 42 | func testRubyInterop() { 43 | doErrorFree { 44 | try Ruby.require(filename: Helpers.fixturePath("methods.rb")) 45 | 46 | guard let instance = RbObject(ofClass: "MethodsTest") else { 47 | XCTFail("Couldn't create instance") 48 | return 49 | } 50 | 51 | // Get Ruby array + convert to Swift 52 | 53 | let arrObj = try instance.call("get_num_array") 54 | guard let array = Array(arrObj) else { 55 | XCTFail("Couldn't convert to Swift array") 56 | return 57 | } 58 | XCTAssertEqual([1, 2, 3], array) 59 | 60 | // Pass Swift array to Ruby 61 | let sumObj = try instance.call("sum_array", args: [[1, 2, 3]]) 62 | XCTAssertEqual(1 + 2 + 3, Int(sumObj)) 63 | } 64 | } 65 | 66 | /// Heterogeneous arrays (+ to_a behavior of Array) 67 | func testMixedArrays() { 68 | doErrorFree { 69 | try Ruby.require(filename: Helpers.fixturePath("methods.rb")) 70 | 71 | guard let instance = RbObject(ofClass: "MethodsTest") else { 72 | XCTFail("Couldn't create instance") 73 | return 74 | } 75 | 76 | if let intArray = Array(instance) { 77 | XCTFail("Managed to convert array to ints: \(intArray)") 78 | return 79 | } 80 | 81 | guard let objArray = Array(instance) else { 82 | XCTFail("Couldn't convert to obj array") 83 | return 84 | } 85 | 86 | XCTAssertEqual(1, Int(objArray[0])) 87 | XCTAssertEqual("two", String(objArray[1])) 88 | XCTAssertEqual(3.0, Double(objArray[2])) 89 | } 90 | } 91 | 92 | /// Array literal 93 | func testArrayLiteral() { 94 | let obj: RbObject = [1, 2, 3] 95 | XCTAssertEqual([1, 2, 3], Array(obj)) 96 | } 97 | 98 | /// Nonconvertible (tricky!) 99 | func testNoArrayConversion() { 100 | doErrorFree { 101 | try Ruby.require(filename: Helpers.fixturePath("nonconvert.rb")) 102 | 103 | guard let instance = RbObject(ofClass: "NotArrayable") else { 104 | XCTFail("Couldn't create instance") 105 | return 106 | } 107 | 108 | if let arr = Array(instance) { 109 | XCTFail("Managed to arrayify unarrayifyable: \(arr)") 110 | return 111 | } 112 | } 113 | } 114 | 115 | /// Quick ArraySlice check 116 | func testSlice() { 117 | let array = [1, 2, 3] 118 | let slice = array[1...2] 119 | let rbArray = RbObject(slice) 120 | guard let backArray = Array(rbArray) else { 121 | XCTFail("Couldn't get the array back") 122 | return 123 | } 124 | XCTAssertEqual(slice, ArraySlice(backArray)) 125 | 126 | if let swSlice = ArraySlice(rbArray) { 127 | XCTFail("Managed to convert to Swift slice: \(swSlice)") 128 | return 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestCallable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestCallable.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import CRuby 10 | @testable /* Qtrue */ import RubyGateway 11 | 12 | /// Message send tests 13 | class TestCallable: XCTestCase { 14 | 15 | // 'global' function 16 | func testCallGlobal() { 17 | doErrorFree { 18 | let res = try Ruby.call("sprintf", args: ["num=%d", 100]) 19 | XCTAssertEqual("num=100", String(res)) 20 | } 21 | } 22 | 23 | // Missing global 24 | func testCallGlobalFailure() { 25 | doError { 26 | let res = try Ruby.call("does_not_exist", args: [1, 2, 3]) 27 | XCTFail("Managed to get \(res) back from invalid global call") 28 | } 29 | } 30 | 31 | // Get a new instance of MethodsTest 32 | private func getNewMethodTest() -> RbObject { 33 | return doErrorFree(fallback: .nilObject) { 34 | try Ruby.require(filename: Helpers.fixturePath("methods.rb")) 35 | return RbObject(ofClass: "MethodsTest")! 36 | } 37 | } 38 | 39 | // attribute 40 | func testAttribute() { 41 | let obj = getNewMethodTest() 42 | 43 | let attrName = "property" 44 | 45 | doErrorFree { 46 | let val = try obj.getAttribute(attrName) 47 | XCTAssertEqual("Default", String(val)) 48 | 49 | let newVal = "Changed" 50 | 51 | try obj.setAttribute(attrName, newValue: newVal) 52 | let val2 = try obj.getAttribute(attrName) 53 | XCTAssertEqual(newVal, String(val2)) 54 | } 55 | } 56 | 57 | // call 58 | func testVoidCall() { 59 | let obj = getNewMethodTest() 60 | 61 | doErrorFree { 62 | let val = try obj.call("noArgsMethod") 63 | XCTAssertTrue(val.isNil) 64 | } 65 | } 66 | 67 | // invalid call - bad message 68 | func testInvalidCall() { 69 | let obj = getNewMethodTest() 70 | 71 | doError { 72 | let val = try obj.call("does-not-exist") 73 | XCTFail("Managed to get \(val) out of invalid method") 74 | } 75 | 76 | doError { 77 | let val = try obj.call("") 78 | XCTFail("Managed to get \(val) out of invalid method") 79 | } 80 | } 81 | 82 | // multi-arg-type call 83 | func testMultiArgCall() { 84 | let obj = getNewMethodTest() 85 | 86 | doErrorFree { 87 | let val = try obj.call("threeArgsMethod", args: ["str", 8, 1.94e1]) 88 | XCTAssertTrue(val.isTruthy) 89 | } 90 | } 91 | 92 | // missing args 93 | func testMissingArgCall() { 94 | let obj = getNewMethodTest() 95 | 96 | doError { 97 | let val = try obj.call("threeArgsMethod", args: ["str", 8]) 98 | XCTFail("Managed to get \(val) with incomplete args") 99 | } 100 | } 101 | 102 | // 'get' chaining 103 | func testGetChaining() { 104 | let _ = getNewMethodTest() 105 | 106 | doErrorFree { 107 | let v = try Ruby.get("MethodsTest").get("classMethod") 108 | XCTAssertTrue(Bool(v)!) 109 | } 110 | } 111 | 112 | // kw args 113 | func testKwArgs() { 114 | let obj = getNewMethodTest() 115 | 116 | doErrorFree { 117 | let v = try obj.call("kwArgsMethod", args: [214], kwArgs: ["aSecond": 32]) 118 | XCTAssertEqual(214 + 32 * 1, Int(v)) 119 | } 120 | } 121 | 122 | // kw args, dup 123 | func testDupKwArgs() { 124 | let obj = getNewMethodTest() 125 | 126 | doErrorFree { 127 | do { 128 | let v = try obj.call("kwArgsMethod", args: [214], kwArgs: ["aSecond": 32, "aSecond": 38]) 129 | XCTFail("Managed to pass duplicate keyword args to Ruby, got \(v)") 130 | } catch RbError.duplicateKwArg(let key) { 131 | XCTAssertEqual("aSecond", key) 132 | } 133 | } 134 | } 135 | 136 | // call via symbol 137 | func testCallViaSymbol() { 138 | let obj = getNewMethodTest() 139 | 140 | doErrorFree { 141 | let methodSym = try obj.getInstanceVar("@doubleMethod") 142 | XCTAssertEqual(.T_SYMBOL, methodSym.rubyType) 143 | 144 | let val = 38 145 | let result = try obj.call(symbol: methodSym, args: [38]) 146 | XCTAssertEqual(val * 2, Int(result)) 147 | } 148 | } 149 | 150 | // call via symbol - error case 151 | func testCallViaSymbolNotSymbol() { 152 | let obj = getNewMethodTest() 153 | 154 | doErrorFree { 155 | do { 156 | let res = try obj.call(symbol: RbObject("double"), args:[100]) 157 | XCTFail("Managed to call something: \(res)") 158 | } catch RbError.badType(_) { 159 | } 160 | } 161 | } 162 | 163 | // call with a Swift block 164 | func testCallWithBlock() { 165 | let obj = getNewMethodTest() 166 | 167 | doErrorFree { 168 | let expectedRes = "answer" 169 | 170 | let res = try obj.call("yielder", kwArgs: ["value": 22]) { args in 171 | XCTAssertEqual(2, args.count) 172 | XCTAssertEqual(22, Int(args[0])) 173 | XCTAssertEqual("fish", String(args[1])) 174 | return RbObject(expectedRes) 175 | } 176 | XCTAssertEqual(expectedRes, String(res)) 177 | 178 | // sym version 179 | let res2 = try obj.call(symbol: RbSymbol("yielder"), kwArgs: ["value": 22], blockRetention: .none) { args in 180 | XCTAssertEqual(2, args.count) 181 | XCTAssertEqual(22, Int(args[0])) 182 | XCTAssertEqual("fish", String(args[1])) 183 | return RbObject(expectedRes) 184 | } 185 | XCTAssertEqual(expectedRes, String(res2)) 186 | } 187 | } 188 | 189 | // call with a Proc'd Swift block 190 | func testCallWithProcBlock() { 191 | let obj = getNewMethodTest() 192 | 193 | doErrorFree { 194 | let expectedRes = "answer" 195 | let proc = RbObject() { args in 196 | XCTAssertEqual(2, args.count) 197 | XCTAssertEqual(22, Int(args[0])) 198 | XCTAssertEqual("fish", String(args[1])) 199 | return RbObject(expectedRes) 200 | } 201 | let res = try obj.call("yielder2", block: proc) 202 | XCTAssertEqual(expectedRes, String(res)) 203 | 204 | // sym version 205 | let res2 = try obj.call(symbol: RbSymbol("yielder2"), block: proc) 206 | XCTAssertEqual(expectedRes, String(res2)) 207 | } 208 | } 209 | 210 | // yield kw args to a proc block 211 | func testCallWithProcBlockKwArgs() { 212 | let obj = getNewMethodTest() 213 | 214 | doErrorFree { 215 | let expectedRes = 22 216 | let proc = try Ruby.eval(ruby: "Proc.new { |a:| a }") 217 | let res = try obj.call("yielder3", args: [expectedRes], block: proc) 218 | XCTAssertEqual(expectedRes, Int(res)) 219 | } 220 | } 221 | 222 | // Store a Swift block and later call it 223 | func testStoredSwiftBlock() { 224 | doErrorFree { 225 | let obj = getNewMethodTest() 226 | 227 | var counter = 0 228 | 229 | try obj.call("store_block", blockRetention: .self) { args in 230 | counter += 1 231 | return .nilObject 232 | } 233 | 234 | XCTAssertEqual(0, counter) 235 | 236 | try obj.call("call_block") 237 | XCTAssertEqual(1, counter) 238 | } 239 | } 240 | 241 | // Nil magic 242 | func testNilValue() { 243 | let obj = getNewMethodTest() 244 | 245 | doErrorFree { 246 | try obj.call("expectsNil", args: [nil]) 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestCollection.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestThreads.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | import Foundation 11 | 12 | class TestCollection: XCTestCase { 13 | 14 | // check it basically works 15 | func testSequence() { 16 | let arr: RbObject = [1, 2, 3, 4] 17 | let sum = arr.collection.map { $0 * 2 }.reduce(0) { $0 + $1 } 18 | XCTAssertEqual(20, Int(sum)) 19 | } 20 | 21 | // check mutable stuff works OK 22 | func testMutable() { 23 | let arr: RbObject = [4, 3, 2, 1] 24 | arr.collection[2] = 8 25 | XCTAssertEqual([4, 3, 8, 1], Array(arr)) 26 | arr.collection[0..<2].sort() 27 | XCTAssertEqual([3, 4, 8, 1], Array(arr)) 28 | } 29 | 30 | // check range setter passed thru OK, same cardinality 31 | func testRangeReplace() { 32 | let arr: RbObject = [1, 2, 3, 4] 33 | arr.collection.replaceSubrange(0..<2, with: [8, 9]) 34 | XCTAssertEqual([8, 9, 3, 4], Array(arr)) 35 | } 36 | 37 | // check range setter passed thru OK, > cardinality 38 | func testRangeReplaceMore() { 39 | let arr: RbObject = [1, 2, 3, 4] 40 | arr.collection.replaceSubrange(0...1, with: [8, 9, 10, 11]) 41 | XCTAssertEqual([8, 9, 10, 11, 3, 4], Array(arr)) 42 | } 43 | 44 | // check range setter passed thru OK, < cardinality 45 | func testRangeReplaceLess() { 46 | let arr: RbObject = [1, 2, 3, 4] 47 | 48 | arr.collection.replaceSubrange(0...1, with: [5]) 49 | XCTAssertEqual([5, 3, 4], Array(arr)) 50 | 51 | arr.collection.removeSubrange(1...2) 52 | XCTAssertEqual([5], Array(arr)) 53 | } 54 | 55 | // constructivist API, can't avoid supporting from protocols 56 | func testConstructivist() { 57 | let el = "Fish" 58 | let count = 3 59 | let coll = RbObjectCollection(repeating: RbObject(el), count: count) 60 | XCTAssertEqual(Array(repeating: el, count: count), Array(coll.rubyObject)) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestComplex.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestComplex.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | class TestComplex: XCTestCase { 12 | 13 | // Round trip the types 14 | func testRoundTrip() { 15 | let swiftNum = RbComplex(real: 1, imaginary: 1) 16 | let obj = RbObject(swiftNum) 17 | XCTAssertEqual("Complex", String(try obj.get("class"))) 18 | 19 | guard let roundTripNum = RbComplex(obj) else { 20 | XCTFail("Couldn't convert complex number back to Swift") 21 | return 22 | } 23 | 24 | XCTAssertEqual(swiftNum.real, roundTripNum.real) 25 | XCTAssertEqual(swiftNum.imaginary, roundTripNum.imaginary) 26 | } 27 | 28 | // More sophisticated conversion 29 | func testConversion() { 30 | let real = 1.2 31 | let imaginary = 4 32 | let complexStr = "\(real)+\(imaginary)i" 33 | 34 | guard let num = RbComplex(complexStr) else { 35 | XCTFail("Couldn't create complex number") 36 | return 37 | } 38 | 39 | XCTAssertEqual(real, num.real) 40 | XCTAssertEqual(Double(imaginary), num.imaginary) 41 | } 42 | 43 | // Error case 44 | func testUnconvertible() { 45 | let someProc = RbObject() { args in .nilObject } 46 | if let num = RbComplex(someProc) { 47 | XCTFail("Managed to convert proc to complex: \(num)") 48 | return 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestConstants.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestConstants.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | /// Ruby constant access 12 | class TestConstants: XCTestCase { 13 | 14 | func testConstantAccess() { 15 | doErrorFree { 16 | let _ = try Ruby.require(filename: Helpers.fixturePath("nesting.rb")) 17 | 18 | let outerModule = try Ruby.getConstant("Outer") 19 | XCTAssertEqual(.T_MODULE, outerModule.rubyType) 20 | 21 | let outerConstant = try outerModule.getConstant("OUTER_CONSTANT") 22 | XCTAssertEqual(.T_FIXNUM, outerConstant.rubyType) 23 | } 24 | } 25 | 26 | private func testBadName(_ name: String) { 27 | doErrorFree { 28 | do { 29 | let const = try Ruby.getConstant(name) 30 | XCTFail("Managed to find constant called '\(name)': \(const)") 31 | } catch RbError.badIdentifier(_, let id) { 32 | XCTAssertEqual(name, id) 33 | } 34 | } 35 | } 36 | 37 | func testConstantName() { 38 | testBadName("lowercase") 39 | testBadName("") 40 | testBadName("::") 41 | } 42 | 43 | func testNestedConstantAccess() { 44 | doErrorFree { 45 | let _ = try Ruby.require(filename: Helpers.fixturePath("nesting.rb")) 46 | 47 | let innerClass = try Ruby.getClass("Outer::Middle::Inner") 48 | XCTAssertEqual(.T_CLASS, innerClass.rubyType) 49 | } 50 | } 51 | 52 | func testPopupConstantAccess() { 53 | doErrorFree { 54 | let _ = try Ruby.require(filename: Helpers.fixturePath("nesting.rb")) 55 | 56 | let innerClass = try Ruby.getClass("Outer::Middle::Inner") 57 | 58 | let _ = try innerClass.getConstant("Outer") 59 | } 60 | } 61 | 62 | func testFailedConstantAccess() { 63 | doError { 64 | let _ = try Ruby.require(filename: Helpers.fixturePath("nesting.rb")) 65 | 66 | let outerModule = try Ruby.getConstant("Fish") 67 | XCTFail("Managed to find 'Fish' constant: \(outerModule)") 68 | } 69 | 70 | let middleModule = try! Ruby.getConstant("Outer::Middle") 71 | doError { 72 | let outerModule = try middleModule.getConstant("Outer::Inner") 73 | XCTFail("Constant scope resolved weirdly - \(outerModule)") 74 | } 75 | 76 | doError { 77 | let innerConstant = try Ruby.getConstant("Outer::Middle::Fish") 78 | XCTFail("Managed to find 'Fish' constant: \(innerConstant)") 79 | } 80 | } 81 | 82 | func testNotAClass() { 83 | doErrorFree { 84 | do { 85 | let _ = try Ruby.require(filename: Helpers.fixturePath("nesting.rb")) 86 | 87 | let notClass = try Ruby.getClass("Outer") 88 | XCTFail("Managed to get a class for module Outer: \(notClass)") 89 | } catch RbError.badType(_) { 90 | // OK 91 | } 92 | } 93 | } 94 | 95 | func testSetTop() { 96 | doErrorFree { 97 | let constName = "NU_CONST" 98 | let constVal = 48 99 | 100 | try Ruby.setConstant(constName, newValue: constVal) 101 | 102 | let readConst = try Ruby.getConstant(constName) 103 | XCTAssertEqual(constVal, Int(readConst)) 104 | } 105 | } 106 | 107 | func testSetNested() { 108 | doErrorFree { 109 | try Ruby.require(filename: Helpers.fixturePath("nesting.rb")) 110 | 111 | let midModule = try Ruby.get("Outer::Middle") 112 | 113 | let constName = "INNER_CONSTANT_2" 114 | let constVal = "Peace" 115 | 116 | let setVal = try midModule.setConstant("Inner::\(constName)", newValue: constVal) 117 | XCTAssertEqual(constVal, String(setVal)) 118 | 119 | let rbReadVal = try Ruby.eval(ruby: "Outer::Middle::Inner::\(constName)") 120 | XCTAssertEqual(constVal, String(rbReadVal)) 121 | } 122 | } 123 | 124 | func testSetErrors() { 125 | doError { 126 | try Ruby.setConstant("unConstantName", newValue: 22) 127 | XCTFail("Created badly named constant") 128 | } 129 | 130 | doError { 131 | try Ruby.setConstant("Imaginary::Constant", newValue: 22) 132 | XCTFail("Created badly namedspaced constant") 133 | } 134 | 135 | let obj = RbObject(22) 136 | doError { 137 | try obj.setConstant("SomeConstant", newValue: 22) 138 | XCTFail("Created constant inside non-class object") 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestDemo.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestDemo.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | /// Some higher-level demos - fail if gems are missing 12 | class TestDemo: XCTestCase { 13 | 14 | func testRouge() throws { 15 | doErrorFree { 16 | try Ruby.require(filename: "rouge") 17 | // Careful to avoid String methods that are unimplemented on Linux.... 18 | let swiftText = try String(contentsOfFile: URL(fileURLWithPath: #filePath).path, encoding: .utf8) 19 | 20 | let html = try Ruby.get("Rouge").call("highlight", args: [swiftText, "swift", "html"]) 21 | 22 | XCTAssertTrue(String(html)!.contains("}")) 23 | } 24 | } 25 | 26 | func testDemo() { 27 | doErrorFree { 28 | try Ruby.require(filename: Helpers.fixturePath("demo.rb")) 29 | 30 | // Create a named student 31 | let student = RbObject(ofClass: "Academy::Student", kwArgs: ["name": "barney"])! 32 | try XCTAssertEqual("barney", String(student.get("name"))) 33 | 34 | // Fix their name! 35 | try student.setInstanceVar("@name", newValue: "Barney") 36 | try XCTAssertEqual("Barney", String(student.get("name"))) 37 | 38 | // Manually add some reading test results 39 | let readingSubject = RbSymbol("reading") 40 | 41 | try student.call("add_score", args: [readingSubject, 30]) 42 | try student.call("add_score", args: [readingSubject, 36.5]) 43 | 44 | guard let avgReadingScore = try Double(student.call("mean_score_for_subject", args: [readingSubject])) else { 45 | XCTFail("Couldn't get double result out") 46 | return 47 | } 48 | XCTAssertEqual(33.25, avgReadingScore) 49 | 50 | // Create a year group + put barney in it 51 | let yearGroup = RbObject(ofClass: "Academy::YearGroup")! 52 | try yearGroup.call("add_student", args: [student]) 53 | 54 | // do test - needs block 55 | 56 | try yearGroup.call("report") 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestDictionaries.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestDictionaries.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | class TestDictionaries: XCTestCase { 12 | 13 | func testRoundTrip() { 14 | let dict = [1: "One", 2: "Two", 3: "Three"] 15 | 16 | let hashObj = RbObject(dict) 17 | dict.forEach { ele in 18 | XCTAssertEqual(ele.value, String(hashObj[ele.key])) 19 | } 20 | 21 | guard let backDict = Dictionary(hashObj) else { 22 | XCTFail("Couldn't convert back to Swift") 23 | return 24 | } 25 | XCTAssertEqual(dict, backDict) 26 | } 27 | 28 | private func getSymNumHash(method: String = "get_sym_num_hash") -> RbObject { 29 | return doErrorFree(fallback: .nilObject) { 30 | try Ruby.require(filename: Helpers.fixturePath("methods.rb")) 31 | 32 | guard let instance = RbObject(ofClass: "MethodsTest") else { 33 | XCTFail("Couldn't create instance") 34 | return .nilObject 35 | } 36 | 37 | return try instance.call(method) 38 | } 39 | } 40 | 41 | func testSwiftTypeConversion() { 42 | let hashObj = getSymNumHash() 43 | 44 | guard let _ = Dictionary(hashObj) else { 45 | XCTFail("Can't convert to right type") 46 | return 47 | } 48 | 49 | if let badHash1 = Dictionary(hashObj) { 50 | XCTFail("Managed to convert String key to Double: \(badHash1)") 51 | return 52 | } 53 | 54 | if let badHash2 = Dictionary>(hashObj) { 55 | XCTFail("Managed to convert Int value to Array: \(badHash2)") 56 | return 57 | } 58 | } 59 | 60 | func testDuplicateKey() { 61 | let hashObj = getSymNumHash(method: "get_ambiguous_hash") 62 | 63 | if let oddHash = Dictionary(hashObj) { 64 | XCTFail("Managed to convert hash: \(oddHash)") 65 | } 66 | } 67 | 68 | func testLiteral() { 69 | let obj: RbObject = [1: "fish", 2: "bucket", 3: "wife", 4: "goat"] 70 | XCTAssertEqual([1: "fish", 2: "bucket", 3: "wife", 4: "goat"], Dictionary(obj)) 71 | } 72 | 73 | func testNoConversion() { 74 | doErrorFree { 75 | try Ruby.require(filename: Helpers.fixturePath("nonconvert.rb")) 76 | 77 | guard let notHashable = RbObject(ofClass: "NotHashable"), 78 | let justToH = RbObject(ofClass: "JustToH"), 79 | let bothToHAndToHash = RbObject(ofClass: "BothToHAndToHash"), 80 | let trapToHash = RbObject(ofClass: "TrapToHash") else { 81 | XCTFail("Couldn't create instance") 82 | return 83 | } 84 | 85 | if let unexpected = Dictionary(notHashable) { 86 | XCTFail("Unexpected conversion: \(unexpected)") 87 | return 88 | } 89 | 90 | XCTAssertEqual([1: 2], Dictionary(justToH)) 91 | 92 | XCTAssertEqual([1: 2], Dictionary(bothToHAndToHash)) 93 | 94 | if let unexpected = Dictionary(trapToHash) { 95 | XCTFail("Unexpected conversion: \(unexpected)") 96 | return 97 | } 98 | } 99 | } 100 | 101 | func testNilConversion() { 102 | doErrorFree { 103 | guard let empty = Dictionary(RbObject.nilObject) else { 104 | XCTFail("Couldn't convert ruby nil to dict") 105 | return 106 | } 107 | XCTAssertEqual(0, empty.count) 108 | } 109 | } 110 | 111 | func testDupRubyConversion() { 112 | let dict = [Helpers.ImpreciseRuby(1) : "One", 113 | Helpers.ImpreciseRuby(2) : "Two"]; 114 | 115 | let rbDict = RbObject(dict) 116 | XCTAssertTrue(rbDict.isNil) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestDynamic.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestDynamic.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | // Tests for dynamic member function 12 | //class skip_TestDynamic: XCTestCase { 13 | // 14 | // /// Getter 15 | // func testDynamicMemberLookupRead() { 16 | // doErrorFree { 17 | // try Ruby.require(filename: Helpers.fixturePath("methods.rb")) 18 | // 19 | // guard let obj = RbObject(ofClass: "MethodsTest") else { 20 | // XCTFail("Couldn't create object") 21 | // return 22 | // } 23 | // 24 | // guard let strObj = obj.property else { 25 | // XCTFail("Couldn't access member 'property'") 26 | // return 27 | // } 28 | // 29 | // XCTAssertEqual("Default", String(strObj)) 30 | // 31 | // if let mysterious = obj.not_a_member { 32 | // XCTFail("Accessed not_a_member: \(mysterious)") 33 | // return 34 | // } 35 | // } 36 | // } 37 | // 38 | // /// Write 39 | // func testDynamicMemberLookupWrite() { 40 | // doErrorFree { 41 | // try Ruby.require(filename: Helpers.fixturePath("methods.rb")) 42 | // 43 | // guard let obj = RbObject(ofClass: "MethodsTest") else { 44 | // XCTFail("Couldn't create object") 45 | // return 46 | // } 47 | // 48 | // let newValue = "Changed it!" 49 | // obj.property = RbObject(newValue) 50 | // 51 | // guard let strObj = obj.property else { 52 | // XCTFail("Couldn't access member 'property'") 53 | // return 54 | // } 55 | // 56 | // XCTAssertEqual(newValue, String(strObj)) 57 | // 58 | // RbError.history.clear() 59 | // obj.bad_property = RbObject(23) 60 | // XCTAssertNotNil(RbError.history.mostRecent) 61 | // 62 | // RbError.history.clear() 63 | // obj.property = nil 64 | // XCTAssertEqual(.nilObject, obj.property) 65 | // } 66 | // } 67 | //} 68 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestErrors.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestErrors.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | @testable /* ErrHistory internals */ import RubyGateway 10 | 11 | /// Tedious tests for error + exception classes 12 | class TestErrors: XCTestCase { 13 | 14 | /// Description vagueness 15 | func testErrorPrinting() { 16 | let errStr = "ErrStr" 17 | let err = RbError.setup(errStr) 18 | XCTAssertTrue(err.description.contains(errStr)) 19 | 20 | let err2 = RbError.badType(errStr) 21 | XCTAssertTrue(err2.description.contains(errStr)) 22 | 23 | let errStrId = "ErrId" 24 | let err3 = RbError.badIdentifier(type: errStr, id: errStrId) 25 | XCTAssertTrue(err3.description.contains(errStr) && err3.description.contains(errStrId)) 26 | 27 | let err4 = RbError.duplicateKwArg(errStr) 28 | XCTAssertTrue(err4.description.contains(errStr)) 29 | 30 | let err5 = RbError.badParameter(errStr) 31 | XCTAssertTrue(err5.description.contains(errStr)) 32 | 33 | let tagVal = Int32(22) 34 | let jmpErr = RbError.rubyJump(tagVal) 35 | XCTAssertTrue(jmpErr.description.contains(String(tagVal))) 36 | } 37 | 38 | /// Need this to avoid nutty 'code will never be executed' warning triggered 39 | /// by "-> Never"... 40 | private func raise(error: RbError) throws { 41 | try RbError.raise(error: error) 42 | } 43 | 44 | /// Error history basic 45 | func testErrorHistory() { 46 | let errStr = "ErrStr" 47 | let err = RbError.setup(errStr) 48 | 49 | try! Ruby.setup() 50 | RbError.history.clear() 51 | 52 | XCTAssertEqual(0, RbError.history.errors.count) 53 | XCTAssertNil(RbError.history.mostRecent) 54 | 55 | try? raise(error: err) 56 | 57 | XCTAssertEqual(1, RbError.history.errors.count) 58 | 59 | guard case let .setup(str) = RbError.history.mostRecent!, 60 | str == errStr else { 61 | XCTFail("Most recent exception wrong.") 62 | return 63 | } 64 | 65 | RbError.history.clear() 66 | 67 | XCTAssertEqual(0, RbError.history.errors.count) 68 | XCTAssertNil(RbError.history.mostRecent) 69 | } 70 | 71 | /// Error history wrap 72 | func testErrorHistoryLen() { 73 | 74 | RbError.history.clear() 75 | 76 | let err = RbError.setup("") 77 | 78 | let MAX_ERRORS = 12 79 | 80 | for n in 1...MAX_ERRORS { 81 | try? raise(error: err) 82 | XCTAssertEqual(n, RbError.history.errors.count) 83 | } 84 | 85 | try? raise(error: RbError.duplicateKwArg("")) 86 | XCTAssertEqual(MAX_ERRORS, RbError.history.errors.count) 87 | 88 | guard case .duplicateKwArg(_) = RbError.history.mostRecent! else { 89 | XCTFail("Most recent exception wrong.") 90 | return 91 | } 92 | } 93 | 94 | /// Ruby exception details 95 | func testRubyException() { 96 | RbError.history.clear() 97 | 98 | doErrorFree { 99 | try Ruby.require(filename: Helpers.fixturePath("raising.rb")) 100 | 101 | do { 102 | try Ruby.call("raiseString") 103 | XCTFail("Didn't raise") 104 | return 105 | } catch RbError.rubyException(let exn) { 106 | let btstr = exn.backtrace 107 | XCTAssertTrue(btstr[0].contains("raiseString")) 108 | XCTAssertTrue(btstr[0].contains("raising.rb:2")) 109 | XCTAssertEqual("RuntimeError: string", exn.description) 110 | } 111 | } 112 | } 113 | 114 | /// Ruby stack overflow 115 | func testRubyStackOverflow() { 116 | doErrorFree { 117 | try Ruby.require(filename: Helpers.fixturePath("raising.rb")) 118 | 119 | doError { 120 | let v = try Ruby.call("stackSmash") 121 | XCTFail("Got past stack overflow: \(v)") 122 | } 123 | } 124 | } 125 | 126 | /// Ruby exit call 127 | func testRubyExit() { 128 | doErrorFree { 129 | try Ruby.require(filename: Helpers.fixturePath("raising.rb")) 130 | 131 | doError { 132 | let v = try Ruby.call("doExit") 133 | XCTFail("Got past exit call : \(v)") 134 | } 135 | 136 | // Just check we haven't exitted Ruby... 137 | testRubyStackOverflow() 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestFailable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestFailable.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | class TestFailable: XCTestCase { 12 | /// get the ruby code 13 | private func setup() { 14 | try! Ruby.require(filename: Helpers.fixturePath("endtoend.rb")) 15 | } 16 | 17 | private func getInstance() -> RbObject { 18 | return RbObject(ofClass: "RubyGateway::EndToEnd", 19 | args: [1.2], 20 | kwArgs: ["name": "barney"])! 21 | } 22 | 23 | // Constants 24 | func testConstants() { 25 | setup() 26 | 27 | guard let mod = Ruby.failable.getConstant("RubyGateway"), 28 | let cls = mod.failable.getClass("EndToEnd") else { 29 | XCTFail("Couldn't get mod + cls") 30 | return 31 | } 32 | 33 | if let const = cls.failable.getConstant("Nope") { 34 | XCTFail("Managed to get 'Nope': \(const)") 35 | return 36 | } 37 | 38 | guard nil != cls.failable.setConstant("NuConstant", newValue: "Val") else { 39 | XCTFail("Couldn't set constant") 40 | return 41 | } 42 | 43 | if let nuConst = RbObject.nilObject.failable.setConstant("NuConst2", newValue: "Val2") { 44 | XCTFail("Managed to set constant on non-class: \(nuConst)") 45 | return 46 | } 47 | } 48 | 49 | // Call, attrs 50 | func testCallAttrs() { 51 | setup() 52 | 53 | let inst = getInstance() 54 | 55 | guard let name = inst.failable.getAttribute("name"), 56 | let _ = inst.failable.setAttribute("name", newValue: "New \(name)") else { 57 | XCTFail("Couldn't set attribute") 58 | return 59 | } 60 | 61 | guard let _ = inst.failable.call("to_s"), 62 | let _ = inst.failable.get("to_s") else { 63 | XCTFail("Couldn't get/call to_s") 64 | return 65 | } 66 | 67 | if let res = inst.failable.call("no_method") { 68 | XCTFail("Managed to call no_method: \(res)") 69 | return 70 | } 71 | 72 | guard let _ = inst.failable.call(symbol: RbSymbol("to_s")) else { 73 | XCTFail("Couldn't call via symbol") 74 | return 75 | } 76 | } 77 | 78 | // Blocks - Swift 79 | func testBlockCalls() { 80 | setup() 81 | 82 | let inst = getInstance() 83 | 84 | guard let _ = inst.failable.call("give_name", blockCall: { args in 85 | XCTAssertEqual("barney", String(args[0])) 86 | return .nilObject 87 | }) else { 88 | XCTFail("Couldn't pass block") 89 | return 90 | } 91 | 92 | if let res = inst.failable.call("always_raise", blockRetention: .none, blockCall: { args in .nilObject }) { 93 | XCTFail("Managed to avoid raise: \(res)") 94 | return 95 | } 96 | 97 | // symbol 98 | guard let _ = inst.failable.call(symbol: RbSymbol("give_name"), blockCall: { args in 99 | XCTAssertEqual("barney", String(args[0])) 100 | return .nilObject 101 | }) else { 102 | XCTFail("Couldn't pass block") 103 | return 104 | } 105 | 106 | if let res = inst.failable.call(symbol: RbSymbol("always_raise"), blockRetention: .none, blockCall: { args in .nilObject }) { 107 | XCTFail("Managed to avoid raise: \(res)") 108 | return 109 | } 110 | } 111 | 112 | // Blocks - Proc 113 | func testBlockProcCalls() { 114 | setup() 115 | 116 | let inst = getInstance() 117 | 118 | let proc = RbObject() { args in .nilObject } 119 | 120 | guard let _ = inst.failable.call("give_name", block: proc) else { 121 | XCTFail("Couldn't pass proc") 122 | return 123 | } 124 | 125 | if let res = inst.failable.call("always_raise", block: proc) { 126 | XCTFail("Managed to avoid raise: \(res)") 127 | return 128 | } 129 | 130 | // symbol 131 | guard let _ = inst.failable.call(symbol: RbSymbol("give_name"), block: proc) else { 132 | XCTFail("Couldn't pass block") 133 | return 134 | } 135 | 136 | if let res = inst.failable.call(symbol: RbSymbol("always_raise"), block: proc) { 137 | XCTFail("Managed to avoid raise: \(res)") 138 | return 139 | } 140 | } 141 | 142 | // ivars 143 | func testIvars() { 144 | setup() 145 | 146 | let inst = getInstance() 147 | 148 | let ivar = "@ivname" 149 | 150 | guard let val = inst.failable.getInstanceVar(ivar) else { 151 | XCTFail("Couldn't get \(ivar)") 152 | return 153 | } 154 | XCTAssertTrue(val.isNil) 155 | 156 | guard let _ = inst.failable.setInstanceVar(ivar, newValue: 3.14) else { 157 | XCTFail("Managed to get a failure from setinstancevar") 158 | return 159 | } 160 | 161 | if let val = inst.failable.getInstanceVar("bad-name") { 162 | XCTFail("Used bad ivar name: \(val)") 163 | return 164 | } 165 | } 166 | 167 | // cvars 168 | func testCvars() { 169 | setup() 170 | 171 | guard let clazz = Ruby.failable.getClass("RubyGateway::EndToEnd") else { 172 | XCTFail("Couldn't get class") 173 | return 174 | } 175 | 176 | let cvar = "@@mycvar" 177 | 178 | if let cv = clazz.failable.getClassVar(cvar) { 179 | XCTFail("Got invalid Cvar: \(cv)") 180 | return 181 | } 182 | 183 | let cvarVal = 103 184 | 185 | guard let _ = clazz.failable.setClassVar(cvar, newValue: cvarVal), 186 | let reRead = clazz.failable.getClassVar(cvar) else { 187 | XCTFail("Failed to set/get cvar") 188 | return 189 | } 190 | XCTAssertEqual(cvarVal, Int(reRead)) 191 | } 192 | 193 | // globals 194 | func testGlobals() { 195 | setup() 196 | 197 | let gvar = "$mygvar" 198 | 199 | if let gvarVal = Ruby.failable.getGlobalVar("bad_name") { 200 | XCTFail("Managed to get badly named gvar: \(gvarVal)") 201 | return 202 | } 203 | 204 | guard let val = Ruby.failable.getGlobalVar(gvar) else { 205 | XCTFail("Couldn't get \(gvar)") 206 | return 207 | } 208 | XCTAssertTrue(val.isNil) 209 | 210 | let newVal = "NewGVarVal" 211 | 212 | guard let _ = Ruby.failable.setGlobalVar(gvar, newValue: newVal), 213 | let reRead = Ruby.failable.getGlobalVar(gvar) else { 214 | XCTFail("Failed to set/get gvar") 215 | return 216 | } 217 | XCTAssertEqual(newVal, String(reRead)) 218 | } 219 | } 220 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestGlobalVars.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestGlobalVars.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | /// Virtual GVars. 12 | class TestGlobalVars: XCTestCase { 13 | 14 | // read/write, native Swift type 15 | func testVirtualNative() { 16 | doErrorFree { 17 | let initialValue = 22 18 | let newValue = 108 19 | 20 | nonisolated(unsafe) var modelValue = initialValue 21 | 22 | let gvarName = "$myVirtualGlobal" 23 | 24 | try Ruby.defineGlobalVar(gvarName, 25 | get: { modelValue }, 26 | set: { modelValue = $0 }) 27 | 28 | let rbCurrent = try Ruby.eval(ruby: gvarName) 29 | XCTAssertEqual(initialValue, modelValue) 30 | XCTAssertEqual(modelValue, Int(rbCurrent)) 31 | 32 | let _ = try Ruby.eval(ruby: "\(gvarName) = \(newValue)") 33 | XCTAssertEqual(newValue, modelValue) 34 | 35 | // Assign a nonconvertible type, system picks it up 36 | doError { 37 | let _ = try Ruby.eval(ruby: "\(gvarName) = 'fishcakes'") 38 | } 39 | XCTAssertEqual(newValue, modelValue) 40 | } 41 | } 42 | 43 | // read/write, RbObject 44 | func testVirtualObj() { 45 | doErrorFree { 46 | let initialIntValue = 100 47 | let targetStringValue = "Berry" 48 | 49 | nonisolated(unsafe) var wrappedObj = RbObject(initialIntValue) 50 | let gvarName = "$myGlobal" 51 | 52 | try Ruby.defineGlobalVar(gvarName, 53 | get: { wrappedObj }, 54 | set: { wrappedObj = $0 }) 55 | 56 | let rbCurrent = try Ruby.eval(ruby: gvarName) 57 | XCTAssertEqual(initialIntValue, Int(rbCurrent)) 58 | 59 | let _ = try Ruby.eval(ruby: "\(gvarName) = '\(targetStringValue)'") 60 | XCTAssertEqual(targetStringValue, String(wrappedObj)) 61 | } 62 | } 63 | 64 | func testReadonly() { 65 | doErrorFree { 66 | let gvarName = "$myVirtualGlobal" 67 | let modelValue = "Fish" 68 | 69 | try Ruby.defineGlobalVar(gvarName) { modelValue } 70 | 71 | let rbCurrent = try Ruby.eval(ruby: gvarName) 72 | XCTAssertEqual(modelValue, String(rbCurrent)) 73 | 74 | doError { 75 | let answer = try Ruby.eval(ruby: "\(gvarName) = 'Bucket'") 76 | XCTFail("Managed to assign to readonly gvar: \(answer)") 77 | } 78 | } 79 | } 80 | 81 | func testSwiftException() { 82 | doErrorFree { 83 | let gvarName = "$myVirtualGlobal" 84 | 85 | try Ruby.defineGlobalVar(gvarName, 86 | get: { 22 }, 87 | set: { _ in throw RbException(message: "Bad new value!") }) 88 | 89 | doError { 90 | let answer = try Ruby.eval(ruby: "\(gvarName) = 44") 91 | XCTFail("Managed to set unsettable: \(answer)") 92 | } 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestMiscObjTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestMiscObjTypes.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import CRuby 9 | @testable /* various macros */ import RubyGateway 10 | import RubyGatewayHelpers 11 | import XCTest 12 | 13 | /// Misc data type tests 14 | class TestMiscObjTypes: XCTestCase { 15 | 16 | func testNilConstants() { 17 | let nilVal = Qnil 18 | let falseVal = Qfalse 19 | let trueVal = Qtrue 20 | 21 | XCTAssertTrue(rbg_RB_NIL_P(nilVal) != 0) 22 | XCTAssertFalse(rbg_RB_NIL_P(falseVal) != 0) 23 | XCTAssertFalse(rbg_RB_NIL_P(trueVal) != 0) 24 | 25 | XCTAssertFalse(rbg_RB_TEST(nilVal) != 0) 26 | XCTAssertFalse(rbg_RB_TEST(falseVal) != 0) 27 | XCTAssertTrue(rbg_RB_TEST(trueVal) != 0) 28 | 29 | XCTAssertEqual(.T_NIL, TYPE(nilVal)) 30 | XCTAssertEqual(.T_FALSE, TYPE(falseVal)) 31 | XCTAssertEqual(.T_TRUE, TYPE(trueVal)) 32 | } 33 | 34 | // Used to support ExpressibleAsNilLiteral but turns out is not so useful 35 | // and docs say not to do so... 36 | func testNilLiteralPromotion() { 37 | let obj: RbObject = RbObject.nilObject 38 | XCTAssertFalse(obj.isTruthy) 39 | XCTAssertTrue(obj.isNil) 40 | obj.withRubyValue { rubyVal in 41 | XCTAssertEqual(Qnil, rubyVal) 42 | } 43 | } 44 | 45 | private func doTestBoolRoundTrip(_ val: Bool) { 46 | let obj = RbObject(val) 47 | XCTAssertTrue(obj.rubyType == .T_FALSE || obj.rubyType == .T_TRUE) 48 | guard let bool = Bool(obj) else { 49 | XCTFail("Couldn't convert boolean value") 50 | return 51 | } 52 | XCTAssertEqual(val, bool) 53 | } 54 | 55 | func testBoolRoundTrip() { 56 | doTestBoolRoundTrip(true) 57 | doTestBoolRoundTrip(false) 58 | } 59 | 60 | func testFailedBoolConversion() { 61 | let obj = RbObject(rubyValue: Qundef) 62 | if let bool = Bool(obj) { 63 | XCTFail("Converted undef to bool - \(bool)") 64 | } 65 | } 66 | 67 | func testBoolLiteralPromotion() { 68 | let trueObj: RbObject = true 69 | let falseObj: RbObject = false 70 | 71 | XCTAssertEqual(.T_TRUE, trueObj.rubyType) 72 | XCTAssertEqual(.T_FALSE, falseObj.rubyType) 73 | } 74 | 75 | func testSymbols() { 76 | let sym = RbSymbol("name") 77 | XCTAssertEqual("RbSymbol(name)", sym.description) 78 | let obj = sym.rubyObject 79 | XCTAssertEqual(.T_SYMBOL, obj.rubyType) 80 | 81 | if let backSym = RbSymbol(obj) { 82 | XCTFail("Managed to create symbol from object: \(backSym)") 83 | } 84 | } 85 | 86 | func testHashableSymbols() { 87 | let h1 = [RbSymbol("one"): "One", RbSymbol("two"): "Two"] 88 | let h2 = [RbSymbol("one").rubyObject: "One", RbSymbol("two").rubyObject: "Two"] 89 | 90 | let rh1 = h1.rubyObject 91 | let rh2 = h2.rubyObject 92 | XCTAssertEqual(rh1, rh2) 93 | } 94 | 95 | func testBadCoerce() { 96 | doErrorFree { 97 | let obj = RbObject("string") 98 | 99 | doError { 100 | let a: Int = try obj.convert() 101 | XCTFail("Managed to convert string to int: \(a)") 102 | } 103 | 104 | doError { 105 | let a = try obj.convert(to: Int.self) 106 | XCTFail("Managed to convert string to int: \(a)") 107 | } 108 | } 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestObjMethods.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestObjMethods.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | // This is about adding methods to objects. 12 | // TestMethods has the method-call/arg stuff and global functions. 13 | 14 | class TestObjMethods: XCTestCase { 15 | 16 | // Basic function 17 | func testSimple() { 18 | doErrorFree { 19 | try Ruby.require(filename: Helpers.fixturePath("swift_obj_methods.rb")) 20 | 21 | nonisolated(unsafe) var callCount = 0 22 | 23 | let clazz = try Ruby.get("EmptyClass") 24 | 25 | let argsSpec = RbMethodArgsSpec(leadingMandatoryCount: 1) 26 | try clazz.defineMethod("double", argsSpec: argsSpec) { _, method in 27 | callCount += 1 28 | let value = Int(method.args.mandatory[0])! 29 | return RbObject(value * 2) 30 | } 31 | 32 | // Call from Swift 33 | guard let instance = RbObject(ofClass: "EmptyClass") else { 34 | XCTFail("Couldn't create EmptyClass") 35 | return 36 | } 37 | let result = try instance.call("double", args: [1]) 38 | XCTAssertEqual(2, result) 39 | XCTAssertEqual(1, callCount) 40 | 41 | // Call from Ruby 42 | let _ = try Ruby.eval(ruby: "test_simple") 43 | XCTAssertEqual(2, callCount) 44 | } 45 | } 46 | 47 | // Check basic error checking 48 | func testInterfaceErrors() { 49 | doErrorFree { 50 | let clazz = try Ruby.get("Object") 51 | 52 | // bad name checked 53 | doError { 54 | try clazz.defineMethod("BadNameForAMethod") { _, _ in .nilObject } 55 | } 56 | 57 | // define method on non-class thing 58 | let notAClass = RbObject("Not a class") 59 | doError { 60 | try notAClass.defineMethod("myMethod") { _, _ in .nilObject } 61 | } 62 | } 63 | } 64 | 65 | // Check modules work as well as classes 66 | func testModule() { 67 | doErrorFree { 68 | try Ruby.require(filename: Helpers.fixturePath("swift_obj_methods.rb")) 69 | 70 | nonisolated(unsafe) var called = false 71 | 72 | let module = try Ruby.get("EmptyModule") 73 | XCTAssertEqual(RbType.T_MODULE, module.rubyType) 74 | 75 | try module.defineMethod("answer") { _, _ in 76 | called = true 77 | return RbObject("true") 78 | } 79 | 80 | let _ = try Ruby.eval(ruby: "test_module") 81 | XCTAssertTrue(called) 82 | } 83 | } 84 | 85 | // Check 'self' is passed through correctly 86 | func testSelf() { 87 | doErrorFree { 88 | try Ruby.require(filename: Helpers.fixturePath("swift_obj_methods.rb")) 89 | 90 | nonisolated(unsafe) var callCount = 0 91 | 92 | let clazz = try Ruby.get("IdentifiedClass") 93 | try clazz.defineMethod("doubleId") { rbSelf, method in 94 | callCount += 1 95 | let myId = try rbSelf.call("uniqueId") 96 | return myId * 2 97 | } 98 | 99 | let _ = try Ruby.eval(ruby: "test_self_access") 100 | XCTAssertEqual(2, callCount) 101 | } 102 | } 103 | 104 | // Define a Swift method in base, check can access from derived 105 | func testInherited() { 106 | doErrorFree { 107 | try Ruby.require(filename: Helpers.fixturePath("swift_obj_methods.rb")) 108 | 109 | nonisolated(unsafe) var callCount = 0 110 | 111 | let clazz = try Ruby.get("BaseClass") 112 | try clazz.defineMethod("getValue") { rbSelf, method in 113 | callCount += 1 114 | return RbObject(22) 115 | } 116 | 117 | let _ = try Ruby.eval(ruby: "test_inherited") 118 | XCTAssertEqual(2, callCount) 119 | } 120 | } 121 | 122 | // Override a Ruby method with a Swift one 123 | func testOverridden() { 124 | doErrorFree { 125 | try Ruby.require(filename: Helpers.fixturePath("swift_obj_methods.rb")) 126 | 127 | nonisolated(unsafe) var callCount = 0 128 | 129 | let clazz = try Ruby.get("OverriddenClass") 130 | try clazz.defineMethod("getValue") { rbSelf, method in 131 | callCount += 1 132 | return RbObject(22) 133 | } 134 | 135 | let _ = try Ruby.eval(ruby: "test_overridden") 136 | XCTAssertEqual(1, callCount) 137 | } 138 | } 139 | 140 | // Docs example 141 | func testArraySum() { 142 | doErrorFree { 143 | let clazz = try Ruby.get("Array") 144 | try clazz.defineMethod("sum") { rbSelf, _ in 145 | rbSelf.collection.reduce(0, +) 146 | } 147 | 148 | let theArray = [1, 2, 3] 149 | 150 | let arr = RbObject(theArray) 151 | let theSum = try arr.call("sum") 152 | XCTAssertEqual(theArray.reduce(0, +), Int(theSum)) 153 | } 154 | } 155 | 156 | // Simple singleton method 157 | func testSingleton() { 158 | doErrorFree { 159 | let module = try Ruby.get("Math") 160 | nonisolated(unsafe) var called = false 161 | try module.defineSingletonMethod("double", argsSpec: .basic(1)) { _, method in 162 | called = true 163 | return method.args.mandatory[0] * 2 164 | } 165 | 166 | let result = try Ruby.eval(ruby: "Math.double(22)") 167 | XCTAssertEqual(44, result) 168 | XCTAssertTrue(called) 169 | } 170 | } 171 | 172 | // Singleton on instance 173 | func testSingletonInstance() { 174 | doErrorFree { 175 | try Ruby.require(filename: Helpers.fixturePath("swift_obj_methods.rb")) 176 | 177 | guard let obj1 = RbObject(ofClass: "SingSimpleClass") else { 178 | XCTFail("Can't create instance") 179 | return 180 | } 181 | XCTAssertEqual(22, try obj1.call("answer")) 182 | 183 | guard let obj2 = RbObject(ofClass: "SingSimpleClass") else { 184 | XCTFail("Can't create instance") 185 | return 186 | } 187 | XCTAssertEqual(22, try obj2.call("answer")) 188 | 189 | try obj1.defineSingletonMethod("answer") { rbSelf, method in 190 | return RbObject(50) 191 | } 192 | 193 | XCTAssertEqual(50, try obj1.call("answer")) 194 | XCTAssertEqual(22, try obj2.call("answer")) 195 | 196 | guard let obj3 = RbObject(ofClass: "SingSimpleClass") else { 197 | XCTFail("Can't create instance") 198 | return 199 | } 200 | XCTAssertEqual(22, try obj3.call("answer")) 201 | } 202 | } 203 | 204 | // Validate self is correct - inheritance case too 205 | func testSingletonDerived() { 206 | doErrorFree { 207 | try Ruby.require(filename: Helpers.fixturePath("swift_obj_methods.rb")) 208 | 209 | nonisolated(unsafe) var called = false 210 | 211 | let clazz = try Ruby.get("SingBase") 212 | try clazz.defineSingletonMethod("value2") { rbSelf, _ in 213 | called = true 214 | let clazzName = String(rbSelf) 215 | XCTAssertEqual("SingDerived", clazzName) 216 | return RbObject(10) 217 | } 218 | 219 | let _ = try Ruby.eval(ruby: "test_ston_overridden") 220 | XCTAssertTrue(called) 221 | } 222 | } 223 | 224 | // Test calling overridden method 225 | func testCallSuper() { 226 | doErrorFree { 227 | try Ruby.require(filename: Helpers.fixturePath("swift_obj_methods.rb")) 228 | 229 | let baseClass = try Ruby.get("SuperBase") 230 | let derClass = try Ruby.defineClass("SuperDerived", parent: baseClass) 231 | try derClass.defineMethod("override_me") { _, m in 232 | try m.callSuper() 233 | } 234 | try derClass.defineMethod("override_me_too") { _, m in 235 | try m.callSuper(args: [1], kwArgs: ["b": 4]) 236 | } 237 | try derClass.defineMethod("override_error") { _, m in 238 | try m.callSuper() 239 | } 240 | 241 | let rc = try Ruby.eval(ruby: "test_override_super") 242 | XCTAssertEqual(true, rc) 243 | 244 | doError { 245 | try Ruby.eval(ruby: "test_override_super2") 246 | } 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestOperators.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestOperators.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | /// Some not-very-comprehensive tests for numeric support - at least check the Swift 12 | /// operators are hooked up to the right Ruby ones. 13 | class TestOperators: XCTestCase { 14 | 15 | func testBasicIntegers() { 16 | let aVal = 12 17 | let bVal = -6 18 | 19 | let aValObj = RbObject(exactly: aVal) 20 | let bValObj = RbObject(exactly: bVal) 21 | 22 | XCTAssertEqual(Int(aVal + bVal), Int(aValObj + bValObj)) 23 | 24 | XCTAssertEqual(Int(aVal - bVal), Int(aValObj - bValObj)) 25 | 26 | XCTAssertEqual(Int(aVal * bVal), Int(aValObj * bValObj)) 27 | 28 | XCTAssertEqual(Int(aVal / bVal), Int(aValObj / bValObj)) 29 | 30 | XCTAssertEqual(Int(aVal % bVal), Int(aValObj % bValObj)) 31 | 32 | XCTAssertEqual(-aVal, Int(-aValObj)) 33 | XCTAssertEqual(-bVal, Int(-bValObj)) 34 | 35 | XCTAssertEqual(+aVal, Int(+aValObj)) 36 | XCTAssertEqual(+bVal, Int(+bValObj)) 37 | 38 | XCTAssertEqual(aVal.magnitude, UInt(aValObj.magnitude)) 39 | XCTAssertEqual(bVal.magnitude, UInt(bValObj.magnitude)) 40 | } 41 | 42 | func testMutating() { 43 | var aVal = 3.4 44 | let bVal = 5.8 45 | 46 | var aValObj = RbObject(aVal) 47 | let bValObj = RbObject(bVal) 48 | 49 | XCTAssertEqual(aVal, Double(aValObj)) 50 | XCTAssertEqual(bVal, Double(bValObj)) 51 | 52 | aVal += bVal 53 | aValObj += bValObj 54 | XCTAssertEqual(aVal, Double(aValObj)) 55 | 56 | aVal *= bVal 57 | aValObj *= bValObj 58 | XCTAssertEqual(aVal, Double(aValObj)) 59 | 60 | aVal -= bVal 61 | aValObj -= bValObj 62 | XCTAssertEqual(aVal, Double(aValObj)) 63 | 64 | aVal /= bVal 65 | aValObj /= bValObj 66 | XCTAssertEqual(aVal, Double(aValObj)) 67 | 68 | aVal.formTruncatingRemainder(dividingBy: bVal) 69 | aValObj %= bValObj 70 | XCTAssertEqual(aVal, Double(aValObj)) 71 | } 72 | 73 | func testSubscript() { 74 | doErrorFree { 75 | try Ruby.require(filename: Helpers.fixturePath("methods.rb")) 76 | guard let inst = RbObject(ofClass: "MethodsTest") else { 77 | XCTFail("Couldn't create instance") 78 | return 79 | } 80 | 81 | let val1 = 1 82 | let val2 = 4.5 83 | let str = inst[val1, val2] 84 | XCTAssertEqual("\(val1) \(val2)", String(str)) 85 | 86 | let val3 = "fred" 87 | inst[val1, val2] = RbObject(val3) 88 | XCTAssertEqual("\(val1) \(val2) = \(val3)", String(try inst.getInstanceVar("@subscript_set"))) 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestProcs.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestProcs.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | @testable /* checkIsProc */ import RubyGateway 10 | 11 | /// Proc tests 12 | class TestProcs: XCTestCase { 13 | 14 | /// Manual proc creation 15 | func testManualProc() { 16 | doErrorFree { 17 | var procHappened = false 18 | 19 | guard let proc = (RbObject(ofClass: "Proc", retainBlock: true) { args in 20 | procHappened = true 21 | return .nilObject 22 | }) else { 23 | XCTFail("Couldn't create proc") 24 | return 25 | } 26 | 27 | try proc.call("call") 28 | XCTAssertTrue(procHappened) 29 | 30 | let arity = try proc.getAttribute("arity") 31 | XCTAssertEqual(-1, arity) 32 | } 33 | } 34 | 35 | /// Create and call simple swift proc 36 | func testCall() { 37 | doErrorFree { 38 | let expectedArg0 = "argString" 39 | let expectedArg1 = 102.8 40 | let expectedArgCount = 2 41 | let expectedResult = -7002 42 | 43 | let proc = RbObject() { args in 44 | XCTAssertEqual(expectedArgCount, args.count) 45 | XCTAssertEqual(expectedArg0, String(args[0])) 46 | XCTAssertEqual(expectedArg1, Double(args[1])) 47 | return RbObject(expectedResult) 48 | } 49 | 50 | let result = try proc.rubyObject.call("call", args: [expectedArg0, expectedArg1]) 51 | 52 | XCTAssertEqual(expectedResult, Int(result)) 53 | } 54 | } 55 | 56 | /// Proc detection 57 | func testNotProc() { 58 | let proc = RbObject() { args in .nilObject } 59 | let object = proc.rubyObject 60 | doErrorFree { 61 | try object.checkIsProc() 62 | 63 | do { 64 | try RbObject.nilObject.checkIsProc() 65 | XCTFail("Believe nil is proc") 66 | } catch RbError.badType(_) { 67 | } 68 | } 69 | } 70 | 71 | /// Failable proc conversion 72 | func testProcConversion() { 73 | if let nilProc = RbProc(RbObject.nilObject) { 74 | XCTFail("Managed to wrap 'nil' in a proc: \(nilProc)") 75 | return 76 | } 77 | 78 | guard let _ = RbProc(RbSymbol("something").rubyObject) else { 79 | XCTFail("Couldn't recognize symbol as to_proc supporting") 80 | return 81 | } 82 | } 83 | 84 | /// Procs from Ruby objects - success 85 | func testRubyObjectProc() { 86 | doErrorFree { 87 | /// assertSame( "AAA", Array("aaa").map(&:upcase).pop ) 88 | let testStr = "aaa" 89 | 90 | let symproc = RbProc(object: RbSymbol("upcase")) 91 | 92 | let array = try Ruby.call("Array", args: [testStr]) 93 | 94 | let mappedArr = try array.call("map", block: symproc) 95 | 96 | let mappedVal = try mappedArr.call("pop") 97 | 98 | XCTAssertEqual(testStr.uppercased(), String(mappedVal)) 99 | } 100 | } 101 | 102 | /// Procs from Ruby objects - fail 103 | func testRubyObjectProcFail() { 104 | doErrorFree { 105 | let notAProc = RbProc(object: "upcase") 106 | 107 | let array = try Ruby.call("Array", args: [1]) 108 | 109 | doError { 110 | let mappedArr = try array.call("map", block: notAProc) 111 | XCTFail("Managed to procify a string: \(mappedArr)") 112 | } 113 | } 114 | } 115 | 116 | /// Exception cases: 117 | /// 1) RbError.rubyException thrown. Propagate. 118 | /// 1b) RbException thrown. Propagate. 119 | /// 2) Some other Error thrown. Raise fresh Ruby exception 120 | /// 3) 'break' issued. Do 'break' thing. 121 | 122 | /// 1) Cause Ruby to raise exception by calling from Swift PRoc 123 | /// -> Detect and convert to Swift RbError 124 | /// -> Catch that and re-raise Ruby exception 125 | /// -> Detect that and convert to Swift RbError again, 126 | /// wrapping original Ruby exception. 127 | func testProcRubyException() { 128 | doErrorFree { 129 | let badString = "Nope" 130 | 131 | let proc = RbObject() { args in 132 | // call nonexistant method -> NoMethodError mentioning `badString` 133 | try args[0].call(badString) 134 | } 135 | 136 | do { 137 | try proc.rubyObject.call("call", args: [120]) 138 | XCTFail("Managed to survive call to throwing proc") 139 | } catch RbError.rubyException(let exn) { 140 | // catch the NoMethodError 141 | print("exn.description: \(exn.description)") 142 | print("exn: \(exn)") 143 | XCTAssertTrue(exn.description.contains(badString)) 144 | } 145 | } 146 | } 147 | 148 | /// 1b - throw 'ruby' exception from Swift 149 | func testProcRubyException2() { 150 | doErrorFree { 151 | let msg = "Ruby Exception!" 152 | 153 | let proc = RbObject() { args in 154 | throw RbException(message: msg) 155 | } 156 | 157 | do { 158 | try proc.rubyObject.call("call") 159 | XCTFail("Managed to survive call to throwing proc") 160 | } catch RbError.rubyException(let exn) { 161 | // catch the RbException 162 | XCTAssertTrue(exn.description.contains(msg)) 163 | } 164 | } 165 | } 166 | 167 | /// 2) Some other Error thrown. 168 | func testProcWeirdException() { 169 | struct S: Error {} 170 | doErrorFree { 171 | let proc = RbObject() { args in 172 | throw S() 173 | } 174 | 175 | do { 176 | try proc.rubyObject.call("call") 177 | XCTFail("Managed to survive call to throwing proc") 178 | } catch RbError.rubyException(let exn) { 179 | print("Got Ruby exception \(exn)") 180 | } 181 | } 182 | } 183 | 184 | /// 3) Break. 185 | func testProcBreak() { 186 | doErrorFree { 187 | let array = try Ruby.call("Array", args: [1]) 188 | try array.call("push", args: [2, 3]) 189 | 190 | var counter = 0 191 | 192 | let retVal = try array.call("each") { args in 193 | counter += 1 194 | if args[0] == 2 { 195 | throw RbBreak() 196 | } 197 | return .nilObject 198 | } 199 | 200 | XCTAssertEqual(2, counter) // break forced iter to end early 201 | XCTAssertEqual(RbObject.nilObject, retVal) 202 | 203 | // Now try breaking with a value 204 | counter = 0 205 | 206 | let breakVal = "Answer" 207 | 208 | let retVal2 = try array.call("each") { args in 209 | counter += 1 210 | if args[0] == 2 { 211 | throw RbBreak(with: breakVal) 212 | } 213 | return .nilObject 214 | } 215 | 216 | XCTAssertEqual(2, counter) 217 | XCTAssertEqual(breakVal, String(retVal2)) 218 | } 219 | } 220 | 221 | // Lambda experiments 222 | func skip_testLambda() { 223 | /* Fails in Ruby 3.3 with exception "Kernal.lambda requires literal block". 224 | See Ruby 19777. 225 | 226 | Turns out this has *never* created a lambda -- we end up calling 227 | `rb_block_call_kw()` which makes a `Proc` out of the block before invoking 228 | the method, basically `lambda(Proc.new { ...})` which ends up returning 229 | (on earlier Rubies) that internal `Proc` such that `it.lambda?` is F. 230 | 231 | So you can't actually create a lambda using the C API. There is enough 232 | stuff to expose something special for it in theory. 233 | */ 234 | doErrorFree { 235 | let lambda = try Ruby.call("lambda", blockRetention: .returned) { args in 236 | if args.count != 2 { 237 | throw RbException(message: "Wrong number of args, expected 2 got \(args.count)") 238 | } 239 | return args[0] + args[1] 240 | } 241 | 242 | let result = try lambda.call("call", args: [1,2]) 243 | XCTAssertEqual(3, Int(result)) 244 | 245 | doError { 246 | let result2 = try lambda.call("call", args: [1]) 247 | XCTFail("Managed to call lambda with insufficient args: \(result2)") 248 | } 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestRanges.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestRange.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | class TestRanges: XCTestCase { 12 | 13 | // basic round-tripping 14 | 15 | func testRoundTrip() { 16 | let range = 13..<29 17 | let rbRange = range.rubyObject 18 | XCTAssertEqual(range, Range(rbRange)) 19 | } 20 | 21 | func testRoundTripClosed() { 22 | let range = 13...29 23 | let rbRange = range.rubyObject 24 | XCTAssertEqual(range, ClosedRange(rbRange)) 25 | } 26 | 27 | func testRoundTripCountable() { 28 | let range = 13..<29 29 | let rbRange = range.rubyObject 30 | XCTAssertEqual(range, CountableRange(rbRange)) 31 | } 32 | 33 | func testRoundTripCountableClosed() { 34 | let range = 13...29 35 | let rbRange = range.rubyObject 36 | XCTAssertEqual(range, CountableClosedRange(rbRange)) 37 | } 38 | 39 | // closed/half-open error cases 40 | 41 | private func closedRubyRange() -> RbObject { 42 | return RbObject(ofClass: "Range", args: [5, 103, false])! 43 | } 44 | 45 | private func halfOpenRubyRange() -> RbObject { 46 | return RbObject(ofClass: "Range", args: [5, 103, true])! 47 | } 48 | 49 | func testRangeTypes() { 50 | if let r = Range(closedRubyRange()) { 51 | XCTFail("Made Range out of closed range: \(r)") 52 | return 53 | } 54 | 55 | if let r = CountableRange(closedRubyRange()) { 56 | XCTFail("Made CountableRange out of closed range: \(r)") 57 | return 58 | } 59 | 60 | if let cr = ClosedRange(halfOpenRubyRange()) { 61 | XCTFail("Made ClosedRange out of half-open range: \(cr)") 62 | return 63 | } 64 | 65 | if let cr = CountableClosedRange(halfOpenRubyRange()) { 66 | XCTFail("Made ClosedRange out of half-open range: \(cr)") 67 | return 68 | } 69 | } 70 | 71 | // unconvertable Range 72 | 73 | func testBadRange() { 74 | doErrorFree { 75 | try Ruby.require(filename: Helpers.fixturePath("nonconvert.rb")) 76 | 77 | guard let rangeObj = RbObject(ofClass: "BadRange") else { 78 | XCTFail("Couldn't create bad range") 79 | return 80 | } 81 | 82 | if let r = Range(rangeObj) { 83 | XCTFail("Managed to create backwards range: \(r)") 84 | return 85 | } 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestRational.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestRational.swift 3 | // RubyGateway 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | class TestRational: XCTestCase { 12 | 13 | // Round trip the types 14 | func testRoundTrip() { 15 | let swiftNum = RbRational(numerator: Double.pi, denominator: 2) 16 | let obj = RbObject(swiftNum) 17 | XCTAssertEqual("Rational", String(try obj.get("class"))) 18 | 19 | guard let roundTripNum = RbRational(obj) else { 20 | XCTFail("Couldn't convert rational number back to Swift") 21 | return 22 | } 23 | 24 | // this test is slightly fishy... 25 | XCTAssertEqual(swiftNum.numerator / swiftNum.denominator, 26 | roundTripNum.numerator / roundTripNum.denominator) 27 | } 28 | 29 | // More sophisticated conversion 30 | func testConversion() { 31 | let num = -2 32 | let denom = 3 33 | let ratStr = "\(num)/\(denom)" 34 | 35 | guard let rat = RbRational(ratStr) else { 36 | XCTFail("Couldn't create rational number") 37 | return 38 | } 39 | 40 | XCTAssertEqual(Double(num), rat.numerator) 41 | XCTAssertEqual(Double(denom), rat.denominator) 42 | } 43 | 44 | // Error case 45 | func testUnconvertible() { 46 | let someProc = RbObject() { args in .nilObject } 47 | if let num = RbRational(someProc) { 48 | XCTFail("Managed to convert proc to rational: \(num)") 49 | return 50 | } 51 | } 52 | 53 | // Swift normalization helper 54 | func testSwiftInput() { 55 | let numerator = Double.pi, denominator = 4.0 56 | 57 | let rat1 = RbRational(numerator: numerator, denominator: -denominator) 58 | let rat2 = RbRational(numerator: -numerator, denominator: denominator) 59 | 60 | XCTAssertEqual(rat1.numerator, rat2.numerator) 61 | XCTAssertEqual(rat1.denominator, rat2.denominator) 62 | 63 | let ratObj1 = rat1.rubyObject, ratObj2 = rat2.rubyObject 64 | XCTAssertFalse(ratObj1.isNil) 65 | 66 | XCTAssertEqual(ratObj1, ratObj2) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestSets.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestSets.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | 11 | class TestSets: XCTestCase { 12 | func testRoundTrip() { 13 | let aSet: Set = [1, 2, 3, 4] 14 | let rbSet = RbObject(aSet) 15 | guard let backSet = Set(rbSet) else { 16 | XCTFail("Couldn't convert set back - \(rbSet)") 17 | return 18 | } 19 | XCTAssertEqual(aSet, backSet) 20 | } 21 | 22 | private func getSet(method: String) -> RbObject { 23 | return doErrorFree(fallback: .nilObject) { 24 | try Ruby.require(filename: Helpers.fixturePath("methods.rb")) 25 | 26 | return try Ruby.get("MethodsTest").get(method) 27 | } 28 | } 29 | 30 | func testElementConversion() { 31 | let setObj = getSet(method: "get_str_set") 32 | 33 | guard let _ = Set(setObj) else { 34 | XCTFail("Couldn't convert Ruby set \(setObj)") 35 | return 36 | } 37 | 38 | if let dSet = Set(setObj) { 39 | XCTFail("Managed to convert string set to FP: \(dSet)") 40 | return 41 | } 42 | } 43 | 44 | func testAmbiguousElements() { 45 | let setObj = getSet(method: "get_ambiguous_num_set") 46 | 47 | if let iSet = Set(setObj) { 48 | XCTFail("Managed to convert odd set to Swift: \(iSet)") 49 | return 50 | } 51 | } 52 | 53 | func testAmbiguousRubyConversion() { 54 | let arr = [Helpers.ImpreciseRuby(1), Helpers.ImpreciseRuby(2)] 55 | let set = Set(arr) 56 | XCTAssertEqual(arr.count, set.count) 57 | 58 | let rubyArr = RbObject(arr) 59 | XCTAssertFalse(rubyArr.isNil) 60 | 61 | let rubySet = RbObject(set) 62 | XCTAssertTrue(rubySet.isNil) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestStrings.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestStrings.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | import CRuby 11 | 12 | /// Tests for String helpers 13 | class TestStrings: XCTestCase { 14 | 15 | private func doTestRoundTrip(_ string: String) { 16 | let rubyObj = RbObject(string) 17 | XCTAssertEqual(.T_STRING, rubyObj.rubyType) 18 | 19 | guard let backString = String(rubyObj) else { 20 | XCTFail("Oops, to_s failed??") 21 | return 22 | } 23 | XCTAssertEqual(string, backString) 24 | } 25 | 26 | func testEmpty() { 27 | doTestRoundTrip("") 28 | } 29 | 30 | func testAscii() { 31 | doTestRoundTrip("A test string") 32 | } 33 | 34 | func testUtf8() { 35 | doTestRoundTrip("abë🐽🇧🇷end") 36 | } 37 | 38 | func testUtf8WithNulls() { 39 | doTestRoundTrip("abë\0🐽🇧🇷en\0d") 40 | } 41 | 42 | func testFailedStringConversion() { 43 | try! Ruby.require(filename: Helpers.fixturePath("nonconvert.rb")) 44 | 45 | guard let instance = RbObject(ofClass: "Nonconvert") else { 46 | XCTFail("Couldn't create object") 47 | return 48 | } 49 | XCTAssertEqual(.T_OBJECT, instance.rubyType) 50 | 51 | if let str = String(instance) { 52 | XCTFail("Converted unconvertible: \(str)") 53 | } 54 | let descr = instance.description 55 | XCTAssertNotEqual("", descr) 56 | } 57 | 58 | // to_s, to_str, priority 59 | func testConversion() { 60 | doErrorFree { 61 | try Ruby.require(filename: Helpers.fixturePath("nonconvert.rb")) 62 | 63 | guard let i1 = RbObject(ofClass: "JustToS"), 64 | let i2 = RbObject(ofClass: "BothToSAndToStr") else { 65 | XCTFail("Couldn't create objects") 66 | return 67 | } 68 | 69 | let _ = try i1.convert(to: String.self) 70 | let s2 = try i2.convert(to: String.self) 71 | 72 | XCTAssertEqual("to_str", s2) 73 | } 74 | } 75 | 76 | func testLiteralPromotion() { 77 | let obj: RbObject = "test string" 78 | XCTAssertEqual("test string", String(obj)) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestThreads.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestThreads.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | import Foundation 11 | 12 | class TestThreads: XCTestCase { 13 | 14 | final class Wrp: @unchecked Sendable { 15 | var threadHasRun: Bool 16 | init() { threadHasRun = false } 17 | } 18 | 19 | // Check Ruby threads + GVL works as expected 20 | func testCreateThread() { 21 | doErrorFree { 22 | let threadHasRun = Wrp() 23 | let threadObj = RbThread.create { 24 | XCTAssertTrue(RbThread.isRubyThread()) 25 | XCTAssertFalse(threadHasRun.threadHasRun) 26 | let obj: RbObject = [1,2,3] 27 | print("Other thread ending: \(obj)") 28 | threadHasRun.threadHasRun = true 29 | } 30 | XCTAssertTrue(RbThread.isRubyThread()) 31 | if let threadObj = threadObj { 32 | try threadObj.call("join") 33 | XCTAssertTrue(threadHasRun.threadHasRun) 34 | } else { 35 | XCTFail("Couldn't create thread object") 36 | } 37 | } 38 | } 39 | 40 | // Check Ruby-created thread can drop GVL 41 | func testThreadCanDropGvl() { 42 | doErrorFree { 43 | let threadHasRun = Wrp() 44 | let threadObj = RbThread.create { 45 | XCTAssertFalse(threadHasRun.threadHasRun) 46 | let obj: RbObject = [1, 2, 3] 47 | print("Other thread giving up GVL: \(obj)") 48 | 49 | RbThread.callWithoutGvl() { 50 | print("Section without GVL") 51 | 52 | RbThread.callWithGvl { 53 | let obj2: RbObject = [4, 5, 6] 54 | print("Back in Ruby: \(obj) \(obj2)") 55 | } 56 | } 57 | 58 | print("Back in with GVL") 59 | 60 | threadHasRun.threadHasRun = true 61 | } 62 | if let threadObj = threadObj { 63 | try threadObj.call("join") 64 | XCTAssertTrue(threadHasRun.threadHasRun) 65 | } else { 66 | XCTFail("Couldn't create thread object") 67 | } 68 | } 69 | } 70 | 71 | final class Wrp2: @unchecked Sendable { 72 | var sleeping: Bool 73 | var slept: Bool 74 | var pid: pthread_t? 75 | 76 | init() { 77 | sleeping = false 78 | slept = false 79 | pid = nil 80 | } 81 | } 82 | 83 | // Check interrupt works outwith GVL using UBF_IO 84 | func testThreadCanBeInterruptedWithoutGvl() { 85 | doErrorFree { 86 | let wrp = Wrp2() 87 | 88 | let threadObj = RbThread.create(callback: { 89 | RbThread.callWithoutGvl(unblocking: .io) { 90 | wrp.sleeping = true 91 | let sleepRc = sleep(100) 92 | XCTAssertNotEqual(0, sleepRc) // means sleep(3) interrupted 93 | wrp.slept = true 94 | } 95 | })! 96 | 97 | while !wrp.sleeping { 98 | try Ruby.call("sleep", args: [0.2]) 99 | } 100 | 101 | // Without an unblocking function this kill is ignored 102 | try Ruby.get("Thread").call("kill", args: [threadObj]) 103 | 104 | try threadObj.call("join") 105 | 106 | XCTAssertTrue(wrp.slept) 107 | } 108 | } 109 | 110 | // Check interrupt works outwith GVL doing it manually 111 | func testThreadCanBeInterruptedWithoutGvlManually() { 112 | doErrorFree { 113 | let wrp = Wrp2() 114 | 115 | let threadObj = RbThread.create(callback: { 116 | RbThread.callWithoutGvl(unblocking: .custom({ pthread_kill(wrp.pid!, SIGVTALRM) }), 117 | callback: { 118 | wrp.pid = pthread_self() 119 | wrp.sleeping = true 120 | let sleepRc = sleep(100) 121 | XCTAssertNotEqual(0, sleepRc) // means sleep(3) interrupted 122 | wrp.slept = true 123 | }) 124 | })! 125 | 126 | while !wrp.sleeping { 127 | try Ruby.call("sleep", args: [0.2]) 128 | } 129 | 130 | // Without an unblocking function this kill is ignored 131 | try Ruby.get("Thread").call("kill", args: [threadObj]) 132 | 133 | try threadObj.call("join") 134 | 135 | XCTAssertTrue(wrp.slept) 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /Tests/RubyGatewayTests/TestVars.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TestVars.swift 3 | // RubyGatewayTests 4 | // 5 | // Distributed under the MIT license, see LICENSE 6 | // 7 | 8 | import XCTest 9 | import RubyGateway 10 | import Foundation 11 | 12 | /// Test global etc. vars work 13 | class TestVars: XCTestCase { 14 | 15 | // built-in 16 | func testBuiltinGlobalVar() { 17 | doErrorFree { 18 | guard let rubyPid = try Int32(Ruby.getGlobalVar("$$")) else { 19 | XCTFail("Non-numeric value for $$") 20 | return 21 | } 22 | XCTAssertEqual(getpid(), rubyPid) 23 | } 24 | } 25 | 26 | // new var 27 | func testNewGlobalVar() { 28 | doErrorFree { 29 | let varName = "$MY_GLOBAL" 30 | let obj = try Ruby.getGlobalVar(varName) 31 | XCTAssertTrue(obj.isNil) 32 | 33 | let testValue = 4.1 34 | 35 | try Ruby.setGlobalVar(varName, newValue: testValue) 36 | 37 | // various ways of reading it 38 | func check(_ obj: RbObject) throws { 39 | guard let dblVal = Double(obj) else { 40 | XCTFail("Not floating point: \(obj)") 41 | return 42 | } 43 | XCTAssertEqual(testValue, dblVal) 44 | } 45 | 46 | try check(Ruby.getGlobalVar(varName)) 47 | try check(Ruby.get(varName)) 48 | try check(Ruby.eval(ruby: varName)) 49 | } 50 | } 51 | 52 | // name check... 53 | func testGlobalVarNameCheck() { 54 | doError { 55 | try Ruby.setGlobalVar("LOVELY_GVAR", newValue: 22) 56 | XCTFail("Managed to set global without $name") 57 | } 58 | } 59 | 60 | // instance vars - top self - create/get/set/check 61 | func testTopInstanceVar() { 62 | doErrorFree { 63 | let varName = "@new_main_ivar" 64 | let obj = try Ruby.getInstanceVar(varName) 65 | XCTAssertTrue(obj.isNil) 66 | 67 | let testValue = 1002 68 | 69 | try Ruby.setInstanceVar(varName, newValue: testValue) 70 | 71 | // various ways of reading it 72 | 73 | try XCTAssertEqual(testValue, Int(Ruby.getInstanceVar(varName))) 74 | try XCTAssertEqual(testValue, Int(Ruby.get(varName))) 75 | try XCTAssertEqual(testValue, Int(Ruby.eval(ruby: varName))) 76 | } 77 | } 78 | 79 | // instance vars - regular objects 80 | func testInstanceVar() { 81 | doErrorFree { 82 | try Ruby.require(filename: Helpers.fixturePath("methods.rb")) 83 | 84 | guard let obj = RbObject(ofClass: "MethodsTest") else { 85 | XCTFail("Couldn't create object") 86 | return 87 | } 88 | 89 | let ivarName = "@property" 90 | let ivarObj = try obj.getInstanceVar(ivarName) 91 | XCTAssertEqual("Default", String(ivarObj)) 92 | 93 | let newValue = "Changed" 94 | 95 | try obj.setInstanceVar(ivarName, newValue: newValue) 96 | 97 | try XCTAssertEqual(newValue, String(obj.getInstanceVar(ivarName))) 98 | try XCTAssertEqual(newValue, String(obj.get(ivarName))) 99 | } 100 | } 101 | 102 | // name check... 103 | func testIVarNameCheck() { 104 | doError { 105 | try Ruby.setInstanceVar("LOVELY_IVAR", newValue: 22) 106 | XCTFail("Managed to set ivar without @name") 107 | } 108 | 109 | doError { 110 | try Ruby.setInstanceVar("@@LOVELY_IVAR", newValue: 22) 111 | XCTFail("Managed to set ivar with @@name") 112 | } 113 | } 114 | 115 | // class vars special rule 116 | func testAbsentClassVar() { 117 | doError { 118 | let varName = "@@new_main_cvar" 119 | let obj = try Ruby.getClassVar(varName) 120 | XCTFail("Managed to read non-existent cvar: \(obj)") 121 | } 122 | } 123 | 124 | // cvar round-trip 125 | func testWriteClassVar() { 126 | doErrorFree { 127 | let varName = "@@new_cvar" 128 | let value = 103.8 129 | 130 | // top level is cObject so all works... 131 | 132 | try Ruby.setClassVar(varName, newValue: RbObject(value)) 133 | 134 | try XCTAssertEqual(value, Double(Ruby.getClassVar(varName))) 135 | try XCTAssertEqual(value, Double(Ruby.get(varName))) 136 | 137 | do { 138 | let interactiveRead = try Ruby.eval(ruby: varName) 139 | if Ruby.apiVersion.0 >= 3 { 140 | XCTFail("Managed interactive access to class variable from toplevel") 141 | } 142 | XCTAssertEqual(value, Double(interactiveRead)) 143 | } catch { 144 | if Ruby.apiVersion.0 < 3 { 145 | XCTFail("Couldn't access class variable: \(error)") 146 | } 147 | } 148 | } 149 | } 150 | 151 | // cvar on not-class 152 | func testNotClassClassVar() { 153 | doErrorFree { 154 | do { 155 | let obj = RbObject("AString") 156 | 157 | try obj.setClassVar("@@new_cvar", newValue: 105) 158 | XCTFail("Managed to set class var on non-class") 159 | } catch RbError.badType(_) { 160 | } 161 | } 162 | 163 | doErrorFree { 164 | do { 165 | let obj = RbObject("AString") 166 | 167 | let cvar = try obj.getClassVar("@@new_cvar") 168 | XCTFail("Managed to get class var on non-class: \(cvar)") 169 | } catch RbError.badType(_) { 170 | } 171 | } 172 | } 173 | 174 | // cvar name check 175 | func testCVarNameCheck() { 176 | doError { 177 | try Ruby.setClassVar("LOVELY_CVAR", newValue: 22) 178 | XCTFail("Managed to set cvar without @@name") 179 | } 180 | 181 | doError { 182 | try Ruby.setClassVar("@LOVELY_CVAR", newValue: 22) 183 | XCTFail("Managed to set cvar with @name") 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /docs/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnfairh/RubyGateway/7f360a6e125663443e964486d2b376acfb712fd1/docs/.nojekyll -------------------------------------------------------------------------------- /docs/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 100% 23 | 24 | 25 | 100% 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/docsets/RubyGateway.docset/Contents/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleIdentifier 6 | com.bebop.rubygateway 7 | CFBundleName 8 | RubyGateway 9 | DocSetPlatformFamily 10 | rubygateway 11 | isDashDocset 12 | 13 | dashIndexFilePath 14 | index.html 15 | isJavaScriptEnabled 16 | 17 | DashDocSetDefaultFTSEnabled 18 | 19 | DashDocSetFamily 20 | dashtoc 21 | 22 | DashDocSetFallbackURL 23 | https://johnfairh.github.io/RubyGateway/ 24 | 25 | -------------------------------------------------------------------------------- /docs/docsets/RubyGateway.docset/Contents/Resources/Documents/badge.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | documentation 17 | 18 | 19 | documentation 20 | 21 | 22 | 100% 23 | 24 | 25 | 100% 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/docsets/RubyGateway.docset/Contents/Resources/Documents/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules" : [ 3 | "RubyGateway" 4 | ], 5 | "version" : "1.12.0" 6 | } -------------------------------------------------------------------------------- /docs/docsets/RubyGateway.docset/Contents/Resources/docSet.dsidx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnfairh/RubyGateway/7f360a6e125663443e964486d2b376acfb712fd1/docs/docsets/RubyGateway.docset/Contents/Resources/docSet.dsidx -------------------------------------------------------------------------------- /docs/docsets/RubyGateway.tgz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/johnfairh/RubyGateway/7f360a6e125663443e964486d2b376acfb712fd1/docs/docsets/RubyGateway.tgz -------------------------------------------------------------------------------- /docs/docsets/RubyGateway.xml: -------------------------------------------------------------------------------- 1 | 2 | 6.1.1 3 | https://johnfairh.github.io/RubyGateway/docsets/RubyGateway.tgz 4 | -------------------------------------------------------------------------------- /docs/site.json: -------------------------------------------------------------------------------- 1 | { 2 | "modules" : [ 3 | "RubyGateway" 4 | ], 5 | "version" : "1.12.0" 6 | } --------------------------------------------------------------------------------