├── .gitignore ├── .gitmodules ├── azure-pipelines.yml ├── lanes.lua ├── license.txt ├── premake5.lua ├── readme.md └── source └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | /projects/ 2 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lanes"] 2 | path = lanes 3 | url = https://github.com/LuaLanes/lanes.git 4 | -------------------------------------------------------------------------------- /azure-pipelines.yml: -------------------------------------------------------------------------------- 1 | variables: 2 | MODULE_NAME: lanes.core 3 | DEPENDENCIES: $(System.DefaultWorkingDirectory)/dependencies 4 | GARRYSMOD_COMMON: $(System.DefaultWorkingDirectory)/dependencies/garrysmod_common 5 | GARRYSMOD_COMMON_BRANCH: master 6 | GARRYSMOD_COMMON_REPOSITORY: https://github.com/danielga/garrysmod_common.git 7 | PROJECT_GENERATOR_VERSION: 2 8 | REPOSITORY_DIR: $(System.DefaultWorkingDirectory) 9 | trigger: 10 | batch: true 11 | branches: 12 | include: 13 | - '*' 14 | tags: 15 | include: 16 | - '*' 17 | paths: 18 | include: 19 | - azure-pipelines.yml 20 | - premake5.lua 21 | - source 22 | jobs: 23 | - job: windows 24 | displayName: Windows 25 | pool: 26 | name: Azure Pipelines 27 | vmImage: windows-2022 28 | timeoutInMinutes: 10 29 | variables: 30 | BOOTSTRAP_URL: https://raw.githubusercontent.com/danielga/garrysmod_common/master/build/bootstrap.ps1 31 | BUILD_SCRIPT: $(System.DefaultWorkingDirectory)/dependencies/garrysmod_common/build/build.ps1 32 | COMPILER_PLATFORM: vs2022 33 | PROJECT_OS: windows 34 | PREMAKE5: $(System.DefaultWorkingDirectory)/dependencies/windows/premake-core/premake5.exe 35 | PREMAKE5_URL: https://github.com/danielga/garrysmod_common/releases/download/premake-build%2F5.0.0-beta2/premake-5.0.0-beta2-windows.zip 36 | steps: 37 | - checkout: self 38 | clean: true 39 | fetchDepth: 1 40 | submodules: recursive 41 | - powershell: 'Invoke-Expression ((New-Object System.Net.WebClient).DownloadString("$env:BOOTSTRAP_URL"))' 42 | displayName: Bootstrap 43 | - powershell: '& "$env:BUILD_SCRIPT"' 44 | displayName: Build 45 | - task: CopyFiles@2 46 | displayName: 'Copy files to $(Build.ArtifactStagingDirectory)' 47 | inputs: 48 | SourceFolder: '$(System.DefaultWorkingDirectory)/projects/windows/vs2022' 49 | Contents: '*/Release/*.dll' 50 | TargetFolder: '$(Build.ArtifactStagingDirectory)' 51 | CleanTargetFolder: true 52 | flattenFolders: true 53 | preserveTimestamp: true 54 | - publish: '$(Build.ArtifactStagingDirectory)' 55 | displayName: 'Publish Windows binaries' 56 | artifact: windows 57 | - job: linux 58 | displayName: Linux 59 | pool: 60 | name: Azure Pipelines 61 | vmImage: ubuntu-22.04 62 | container: 63 | image: danielga/steamrt-scout:latest 64 | options: -v /home 65 | timeoutInMinutes: 10 66 | variables: 67 | BOOTSTRAP_URL: https://raw.githubusercontent.com/danielga/garrysmod_common/master/build/bootstrap.sh 68 | BUILD_SCRIPT: $(System.DefaultWorkingDirectory)/dependencies/garrysmod_common/build/build.sh 69 | COMPILER_PLATFORM: gmake 70 | PREMAKE5: $(System.DefaultWorkingDirectory)/dependencies/linux/premake-core/premake5 71 | PROJECT_OS: linux 72 | PREMAKE5_URL: https://github.com/danielga/garrysmod_common/releases/download/premake-build%2F5.0.0-beta2/premake-5.0.0-beta2-linux.tar.gz 73 | CC: gcc-9 74 | CXX: g++-9 75 | AR: gcc-ar-9 76 | NM: gcc-nm-9 77 | RANLIB: gcc-ranlib-9 78 | steps: 79 | - checkout: self 80 | clean: true 81 | fetchDepth: 1 82 | submodules: recursive 83 | - bash: 'curl -s -L "$BOOTSTRAP_URL" | bash' 84 | displayName: Bootstrap 85 | - bash: '$BUILD_SCRIPT' 86 | displayName: Build 87 | - task: CopyFiles@2 88 | displayName: 'Copy files to $(Build.ArtifactStagingDirectory)' 89 | inputs: 90 | SourceFolder: '$(System.DefaultWorkingDirectory)/projects/linux/gmake' 91 | Contents: '*/Release/*.dll' 92 | TargetFolder: '$(Build.ArtifactStagingDirectory)' 93 | CleanTargetFolder: true 94 | flattenFolders: true 95 | preserveTimestamp: true 96 | - publish: '$(Build.ArtifactStagingDirectory)' 97 | displayName: 'Publish Linux binaries' 98 | artifact: linux 99 | - job: macosx 100 | displayName: macOS 101 | pool: 102 | name: Azure Pipelines 103 | vmImage: macOS-11 104 | timeoutInMinutes: 10 105 | variables: 106 | BOOTSTRAP_URL: https://raw.githubusercontent.com/danielga/garrysmod_common/master/build/bootstrap.sh 107 | BUILD_SCRIPT: $(System.DefaultWorkingDirectory)/dependencies/garrysmod_common/build/build.sh 108 | COMPILER_PLATFORM: gmake 109 | PREMAKE5: $(System.DefaultWorkingDirectory)/dependencies/macosx/premake-core/premake5 110 | PROJECT_OS: macosx 111 | PREMAKE5_URL: https://github.com/danielga/garrysmod_common/releases/download/premake-build%2F5.0.0-beta2/premake-5.0.0-beta2-macosx.tar.gz 112 | MACOSX_SDK_URL: https://github.com/phracker/MacOSX-SDKs/releases/download/11.3/MacOSX10.7.sdk.tar.xz 113 | MACOSX_SDK_DIRECTORY: $(System.DefaultWorkingDirectory)/dependencies/macosx/MacOSX10.7.sdk 114 | SDKROOT: $(System.DefaultWorkingDirectory)/dependencies/macosx/MacOSX10.7.sdk 115 | AR: ar 116 | steps: 117 | - checkout: self 118 | clean: true 119 | fetchDepth: 1 120 | submodules: recursive 121 | - bash: 'curl -s -L "$BOOTSTRAP_URL" | bash' 122 | displayName: Bootstrap 123 | - bash: | 124 | sudo xcode-select -s "/Applications/Xcode_11.7.app/Contents/Developer" 125 | $BUILD_SCRIPT 126 | displayName: Build 127 | - task: CopyFiles@2 128 | displayName: 'Copy files to $(Build.ArtifactStagingDirectory)' 129 | inputs: 130 | SourceFolder: '$(System.DefaultWorkingDirectory)/projects/macosx/gmake' 131 | Contents: '*/Release/*.dll' 132 | TargetFolder: '$(Build.ArtifactStagingDirectory)' 133 | CleanTargetFolder: true 134 | flattenFolders: true 135 | preserveTimestamp: true 136 | - publish: '$(Build.ArtifactStagingDirectory)' 137 | displayName: 'Publish macOS binaries' 138 | artifact: macosx 139 | - job: publish 140 | displayName: Publish to GitHub Releases 141 | pool: 142 | name: Azure Pipelines 143 | vmImage: ubuntu-22.04 144 | timeoutInMinutes: 5 145 | dependsOn: 146 | - windows 147 | - linux 148 | - macosx 149 | condition: and(succeeded(), startsWith(variables['Build.SourceBranch'], 'refs/tags/')) 150 | steps: 151 | - download: current 152 | patterns: '**/*.dll' 153 | - task: GitHubRelease@1 154 | displayName: 'Publish GitHub release $(build.sourceBranchName)' 155 | inputs: 156 | gitHubConnection: 'GitHub danielga' 157 | releaseNotesSource: inline 158 | assets: '$(Pipeline.Workspace)/**/*.dll' 159 | addChangeLog: false 160 | -------------------------------------------------------------------------------- /lanes.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- LANES.LUA 3 | -- 4 | -- Multithreading and -core support for Lua 5 | -- 6 | -- Authors: Asko Kauppi 7 | -- Benoit Germain 8 | -- 9 | -- History: see CHANGES 10 | -- 11 | --[[ 12 | =============================================================================== 13 | 14 | Copyright (C) 2007-10 Asko Kauppi 15 | Copyright (C) 2010-13 Benoit Germain 16 | 17 | Permission is hereby granted, free of charge, to any person obtaining a copy 18 | of this software and associated documentation files (the "Software"), to deal 19 | in the Software without restriction, including without limitation the rights 20 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 21 | copies of the Software, and to permit persons to whom the Software is 22 | furnished to do so, subject to the following conditions: 23 | 24 | The above copyright notice and this permission notice shall be included in 25 | all copies or substantial portions of the Software. 26 | 27 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 28 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 29 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 30 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 31 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 32 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 33 | THE SOFTWARE. 34 | 35 | =============================================================================== 36 | ]]-- 37 | 38 | lanes = {} 39 | require "lanes.core" 40 | local core = lanes.core 41 | lanes = nil 42 | 43 | -- Lua 5.1: module() creates a global variable 44 | -- Lua 5.2: module() is gone 45 | -- almost everything module() does is done by require() anyway 46 | -- -> simply create a table, populate it, return it, and be done 47 | local lanesMeta = {} 48 | local lanes = setmetatable( {}, lanesMeta) 49 | 50 | -- this function is available in the public interface until it is called, after which it disappears 51 | lanes.configure = function( settings_) 52 | 53 | -- This check is for sublanes requiring Lanes 54 | -- 55 | -- TBD: We could also have the C level expose 'string.gmatch' for us. But this is simpler. 56 | -- 57 | if not string then 58 | error( "To use 'lanes', you will also need to have 'string' available.", 2) 59 | end 60 | -- Configure called so remove metatable from lanes 61 | setmetatable( lanes, nil) 62 | -- 63 | -- Cache globals for code that might run under sandboxing 64 | -- 65 | local assert = assert( assert) 66 | local string_gmatch = assert( string.gmatch) 67 | local string_format = assert( string.format) 68 | local select = assert( select) 69 | local type = assert( type) 70 | local pairs = assert( pairs) 71 | local tostring = assert( tostring) 72 | local error = assert( error) 73 | 74 | local default_params = 75 | { 76 | nb_keepers = 1, 77 | on_state_create = nil, 78 | shutdown_timeout = 0.25, 79 | with_timers = true, 80 | track_lanes = false, 81 | demote_full_userdata = nil, 82 | verbose_errors = false, 83 | -- LuaJIT provides a thread-unsafe allocator by default, so we need to protect it when used in parallel lanes 84 | allocator = (package.loaded.jit and jit.version) and "protected" or nil 85 | } 86 | local boolean_param_checker = function( val_) 87 | -- non-'boolean-false' should be 'boolean-true' or nil 88 | return val_ and (val_ == true) or true 89 | end 90 | local param_checkers = 91 | { 92 | nb_keepers = function( val_) 93 | -- nb_keepers should be a number > 0 94 | return type( val_) == "number" and val_ > 0 95 | end, 96 | with_timers = boolean_param_checker, 97 | allocator = function( val_) 98 | -- can be nil, "protected", or a function 99 | return val_ and (type( val_) == "function" or val_ == "protected") or true 100 | end, 101 | on_state_create = function( val_) 102 | -- on_state_create may be nil or a function 103 | return val_ and type( val_) == "function" or true 104 | end, 105 | shutdown_timeout = function( val_) 106 | -- shutdown_timeout should be a number >= 0 107 | return type( val_) == "number" and val_ >= 0 108 | end, 109 | track_lanes = boolean_param_checker, 110 | demote_full_userdata = boolean_param_checker, 111 | verbose_errors = boolean_param_checker 112 | } 113 | 114 | local params_checker = function( settings_) 115 | if not settings_ then 116 | return default_params 117 | end 118 | -- make a copy of the table to leave the provided one unchanged, *and* to help ensure it won't change behind our back 119 | local settings = {} 120 | if type( settings_) ~= "table" then 121 | error "Bad parameter #1 to lanes.configure(), should be a table" 122 | end 123 | -- any setting unknown to Lanes raises an error 124 | for setting, _ in pairs( settings_) do 125 | if not param_checkers[setting] then 126 | error( "Unknown parameter '" .. setting .. "' in configure options") 127 | end 128 | end 129 | -- any setting not present in the provided parameters takes the default value 130 | for key, checker in pairs( param_checkers) do 131 | local my_param = settings_[key] 132 | local param 133 | if my_param ~= nil then 134 | param = my_param 135 | else 136 | param = default_params[key] 137 | end 138 | if not checker( param) then 139 | error( "Bad " .. key .. ": " .. tostring( param), 2) 140 | end 141 | settings[key] = param 142 | end 143 | return settings 144 | end 145 | local settings = core.configure and core.configure( params_checker( settings_)) or core.settings 146 | local core_lane_new = assert( core.lane_new) 147 | local max_prio = assert( core.max_prio) 148 | 149 | lanes.ABOUT = 150 | { 151 | author= "Asko Kauppi , Benoit Germain ", 152 | description= "Running multiple Lua states in parallel", 153 | license= "MIT/X11", 154 | copyright= "Copyright (c) 2007-10, Asko Kauppi; (c) 2011-19, Benoit Germain", 155 | version = assert( core.version) 156 | } 157 | 158 | 159 | -- Making copies of necessary system libs will pass them on as upvalues; 160 | -- only the first state doing "require 'lanes'" will need to have 'string' 161 | -- and 'table' visible. 162 | -- 163 | local function WR(str) 164 | io.stderr:write( str.."\n" ) 165 | end 166 | 167 | local function DUMP( tbl ) 168 | if not tbl then return end 169 | local str="" 170 | for k,v in pairs(tbl) do 171 | str= str..k.."="..tostring(v).."\n" 172 | end 173 | WR(str) 174 | end 175 | 176 | 177 | ---=== Laning ===--- 178 | 179 | -- lane_h[1..n]: lane results, same as via 'lane_h:join()' 180 | -- lane_h[0]: can be read to make sure a thread has finished (always gives 'true') 181 | -- lane_h[-1]: error message, without propagating the error 182 | -- 183 | -- Reading a Lane result (or [0]) propagates a possible error in the lane 184 | -- (and execution does not return). Cancelled lanes give 'nil' values. 185 | -- 186 | -- lane_h.state: "pending"/"running"/"waiting"/"done"/"error"/"cancelled" 187 | -- 188 | -- Note: Would be great to be able to have '__ipairs' metamethod, that gets 189 | -- called by 'ipairs()' function to custom iterate objects. We'd use it 190 | -- for making sure a lane has ended (results are available); not requiring 191 | -- the user to precede a loop by explicit 'h[0]' or 'h:join()'. 192 | -- 193 | -- Or, even better, 'ipairs()' should start valuing '__index' instead 194 | -- of using raw reads that bypass it. 195 | -- 196 | ----- 197 | -- lanes.gen( [libs_str|opt_tbl [, ...],] lane_func ) ( [...] ) -> h 198 | -- 199 | -- 'libs': nil: no libraries available (default) 200 | -- "": only base library ('assert', 'print', 'unpack' etc.) 201 | -- "math,os": math + os + base libraries (named ones + base) 202 | -- "*": all standard libraries available 203 | -- 204 | -- 'opt': .priority: int (-3..+3) smaller is lower priority (0 = default) 205 | -- 206 | -- .globals: table of globals to set for a new thread (passed by value) 207 | -- 208 | -- .required: table of packages to require 209 | -- 210 | -- .gc_cb: function called when the lane handle is collected 211 | -- 212 | -- ... (more options may be introduced later) ... 213 | -- 214 | -- Calling with a function parameter ('lane_func') ends the string/table 215 | -- modifiers, and prepares a lane generator. 216 | 217 | local valid_libs = 218 | { 219 | ["package"] = true, 220 | ["table"] = true, 221 | ["io"] = true, 222 | ["os"] = true, 223 | ["string"] = true, 224 | ["math"] = true, 225 | ["debug"] = true, 226 | ["bit32"] = true, -- Lua 5.2 only, ignored silently under 5.1 227 | ["utf8"] = true, -- Lua 5.3 only, ignored silently under 5.1 and 5.2 228 | ["bit"] = true, -- LuaJIT only, ignored silently under PUC-Lua 229 | ["jit"] = true, -- LuaJIT only, ignored silently under PUC-Lua 230 | ["ffi"] = true, -- LuaJIT only, ignored silently under PUC-Lua 231 | -- 232 | ["base"] = true, 233 | ["coroutine"] = true, -- part of "base" in Lua 5.1 234 | ["lanes.core"] = true 235 | } 236 | 237 | local raise_option_error = function( name_, tv_, v_) 238 | error( "Bad '" .. name_ .. "' option: " .. tv_ .. " " .. string_format( "%q", tostring( v_)), 4) 239 | end 240 | 241 | local opt_validators = 242 | { 243 | priority = function( v_) 244 | local tv = type( v_) 245 | return (tv == "number") and v_ or raise_option_error( "priority", tv, v_) 246 | end, 247 | globals = function( v_) 248 | local tv = type( v_) 249 | return (tv == "table") and v_ or raise_option_error( "globals", tv, v_) 250 | end, 251 | package = function( v_) 252 | local tv = type( v_) 253 | return (tv == "table") and v_ or raise_option_error( "package", tv, v_) 254 | end, 255 | required = function( v_) 256 | local tv = type( v_) 257 | return (tv == "table") and v_ or raise_option_error( "required", tv, v_) 258 | end, 259 | gc_cb = function( v_) 260 | local tv = type( v_) 261 | return (tv == "function") and v_ or raise_option_error( "gc_cb", tv, v_) 262 | end 263 | } 264 | 265 | -- PUBLIC LANES API 266 | -- receives a sequence of strings and tables, plus a function 267 | local gen = function( ...) 268 | -- aggregrate all strings together, separated by "," as well as tables 269 | -- the strings are a list of libraries to open 270 | -- the tables contain the lane options 271 | local opt = {} 272 | local libs = nil 273 | 274 | local n = select( '#', ...) 275 | 276 | -- we need at least a function 277 | if n == 0 then 278 | error( "No parameters!", 2) 279 | end 280 | 281 | -- all arguments but the last must be nil, strings, or tables 282 | for i = 1, n - 1 do 283 | local v = select( i, ...) 284 | local tv = type( v) 285 | if tv == "string" then 286 | libs = libs and libs .. "," .. v or v 287 | elseif tv == "table" then 288 | for k, vv in pairs( v) do 289 | opt[k]= vv 290 | end 291 | elseif v == nil then 292 | -- skip 293 | else 294 | error( "Bad parameter " .. i .. ": " .. tv .. " " .. string_format( "%q", tostring( v)), 2) 295 | end 296 | end 297 | 298 | -- the last argument should be a function or a string 299 | local func = select( n, ...) 300 | local functype = type( func) 301 | if functype ~= "function" and functype ~= "string" then 302 | error( "Last parameter not function or string: " .. functype .. " " .. string_format( "%q", tostring( func)), 2) 303 | end 304 | 305 | -- check that the caller only provides reserved library names, and those only once 306 | -- "*" is a special case that doesn't require individual checking 307 | if libs and libs ~= "*" then 308 | local found = {} 309 | for s in string_gmatch(libs, "[%a%d.]+") do 310 | if not valid_libs[s] then 311 | error( "Bad library name: " .. s, 2) 312 | else 313 | found[s] = (found[s] or 0) + 1 314 | if found[s] > 1 then 315 | error( "libs specification contains '" .. s .. "' more than once", 2) 316 | end 317 | end 318 | end 319 | end 320 | 321 | -- validate that each option is known and properly valued 322 | for k, v in pairs( opt) do 323 | local validator = opt_validators[k] 324 | if not validator then 325 | error( (type( k) == "number" and "Unkeyed option: " .. type( v) .. " " .. string_format( "%q", tostring( v)) or "Bad '" .. tostring( k) .. "' option"), 2) 326 | else 327 | opt[k] = validator( v) 328 | end 329 | end 330 | 331 | local priority, globals, package, required, gc_cb = opt.priority, opt.globals, opt.package or package, opt.required, opt.gc_cb 332 | return function( ...) 333 | -- must pass functions args last else they will be truncated to the first one 334 | return core_lane_new( func, libs, priority, globals, package, required, gc_cb, ...) 335 | end 336 | end -- gen() 337 | 338 | ---=== Timers ===--- 339 | 340 | -- PUBLIC LANES API 341 | local timer = function() error "timers are not active" end 342 | local timers = timer 343 | local timer_lane = nil 344 | 345 | -- timer_gateway should always exist, even when the settings disable the timers 346 | local timer_gateway = assert( core.timer_gateway) 347 | 348 | ----- 349 | -- = sleep( [seconds_]) 350 | -- 351 | -- PUBLIC LANES API 352 | local sleep = function( seconds_) 353 | seconds_ = seconds_ or 0.0 -- this causes false and nil to be a valid input, equivalent to 0.0, but that's ok 354 | if type( seconds_) ~= "number" then 355 | error( "invalid duration " .. string_format( "%q", tostring(seconds_))) 356 | end 357 | -- receive data on a channel no-one ever sends anything, thus blocking for the specified duration 358 | return timer_gateway:receive( seconds_, "ac100de1-a696-4619-b2f0-a26de9d58ab8") 359 | end 360 | 361 | 362 | if settings.with_timers ~= false then 363 | 364 | -- 365 | -- On first 'require "lanes"', a timer lane is spawned that will maintain 366 | -- timer tables and sleep in between the timer events. All interaction with 367 | -- the timer lane happens via a 'timer_gateway' Linda, which is common to 368 | -- all that 'require "lanes"'. 369 | -- 370 | -- Linda protocol to timer lane: 371 | -- 372 | -- TGW_KEY: linda_h, key, [wakeup_at_secs], [repeat_secs] 373 | -- 374 | local TGW_KEY= "(timer control)" -- the key does not matter, a 'weird' key may help debugging 375 | local TGW_QUERY, TGW_REPLY = "(timer query)", "(timer reply)" 376 | local first_time_key= "first time" 377 | 378 | local first_time = timer_gateway:get( first_time_key) == nil 379 | timer_gateway:set( first_time_key, true) 380 | 381 | -- 382 | -- Timer lane; initialize only on the first 'require "lanes"' instance (which naturally 383 | -- has 'table' always declared) 384 | -- 385 | if first_time then 386 | 387 | local now_secs = core.now_secs 388 | assert( type( now_secs) == "function") 389 | ----- 390 | -- Snore loop (run as a lane on the background) 391 | -- 392 | -- High priority, to get trustworthy timings. 393 | -- 394 | -- We let the timer lane be a "free running" thread; no handle to it 395 | -- remains. 396 | -- 397 | local timer_body = function() 398 | set_debug_threadname( "LanesTimer") 399 | -- 400 | -- { [deep_linda_lightuserdata]= { [deep_linda_lightuserdata]=linda_h, 401 | -- [key]= { wakeup_secs [,period_secs] } [, ...] }, 402 | -- } 403 | -- 404 | -- Collection of all running timers, indexed with linda's & key. 405 | -- 406 | -- Note that we need to use the deep lightuserdata identifiers, instead 407 | -- of 'linda_h' themselves as table indices. Otherwise, we'd get multiple 408 | -- entries for the same timer. 409 | -- 410 | -- The 'hidden' reference to Linda proxy is used in 'check_timers()' but 411 | -- also important to keep the Linda alive, even if all outside world threw 412 | -- away pointers to it (which would ruin uniqueness of the deep pointer). 413 | -- Now we're safe. 414 | -- 415 | local collection = {} 416 | local table_insert = assert( table.insert) 417 | 418 | local get_timers = function() 419 | local r = {} 420 | for deep, t in pairs( collection) do 421 | -- WR( tostring( deep)) 422 | local l = t[deep] 423 | for key, timer_data in pairs( t) do 424 | if key ~= deep then 425 | table_insert( r, {l, key, timer_data}) 426 | end 427 | end 428 | end 429 | return r 430 | end -- get_timers() 431 | 432 | -- 433 | -- set_timer( linda_h, key [,wakeup_at_secs [,period_secs]] ) 434 | -- 435 | local set_timer = function( linda, key, wakeup_at, period) 436 | assert( wakeup_at == nil or wakeup_at > 0.0) 437 | assert( period == nil or period > 0.0) 438 | 439 | local linda_deep = linda:deep() 440 | assert( linda_deep) 441 | 442 | -- Find or make a lookup for this timer 443 | -- 444 | local t1 = collection[linda_deep] 445 | if not t1 then 446 | t1 = { [linda_deep] = linda} -- proxy to use the Linda 447 | collection[linda_deep] = t1 448 | end 449 | 450 | if wakeup_at == nil then 451 | -- Clear the timer 452 | -- 453 | t1[key]= nil 454 | 455 | -- Remove empty tables from collection; speeds timer checks and 456 | -- lets our 'safety reference' proxy be gc:ed as well. 457 | -- 458 | local empty = true 459 | for k, _ in pairs( t1) do 460 | if k ~= linda_deep then 461 | empty = false 462 | break 463 | end 464 | end 465 | if empty then 466 | collection[linda_deep] = nil 467 | end 468 | 469 | -- Note: any unread timer value is left at 'linda[key]' intensionally; 470 | -- clearing a timer just stops it. 471 | else 472 | -- New timer or changing the timings 473 | -- 474 | local t2 = t1[key] 475 | if not t2 then 476 | t2= {} 477 | t1[key]= t2 478 | end 479 | 480 | t2[1] = wakeup_at 481 | t2[2] = period -- can be 'nil' 482 | end 483 | end -- set_timer() 484 | 485 | ----- 486 | -- [next_wakeup_at]= check_timers() 487 | -- Check timers, and wake up the ones expired (if any) 488 | -- Returns the closest upcoming (remaining) wakeup time (or 'nil' if none). 489 | local check_timers = function() 490 | local now = now_secs() 491 | local next_wakeup 492 | 493 | for linda_deep,t1 in pairs(collection) do 494 | for key,t2 in pairs(t1) do 495 | -- 496 | if key==linda_deep then 497 | -- no 'continue' in Lua :/ 498 | else 499 | -- 't2': { wakeup_at_secs [,period_secs] } 500 | -- 501 | local wakeup_at= t2[1] 502 | local period= t2[2] -- may be 'nil' 503 | 504 | if wakeup_at <= now then 505 | local linda= t1[linda_deep] 506 | assert(linda) 507 | 508 | linda:set( key, now ) 509 | 510 | -- 'pairs()' allows the values to be modified (and even 511 | -- removed) as far as keys are not touched 512 | 513 | if not period then 514 | -- one-time timer; gone 515 | -- 516 | t1[key]= nil 517 | wakeup_at= nil -- no 'continue' in Lua :/ 518 | else 519 | -- repeating timer; find next wakeup (may jump multiple repeats) 520 | -- 521 | repeat 522 | wakeup_at= wakeup_at+period 523 | until wakeup_at > now 524 | 525 | t2[1]= wakeup_at 526 | end 527 | end 528 | 529 | if wakeup_at and ((not next_wakeup) or (wakeup_at < next_wakeup)) then 530 | next_wakeup= wakeup_at 531 | end 532 | end 533 | end -- t2 loop 534 | end -- t1 loop 535 | 536 | return next_wakeup -- may be 'nil' 537 | end -- check_timers() 538 | 539 | local timer_gateway_batched = timer_gateway.batched 540 | set_finalizer( function( err, stk) 541 | if err and type( err) ~= "userdata" then 542 | WR( "LanesTimer error: "..tostring(err)) 543 | --elseif type( err) == "userdata" then 544 | -- WR( "LanesTimer after cancel" ) 545 | --else 546 | -- WR("LanesTimer finalized") 547 | end 548 | end) 549 | while true do 550 | local next_wakeup = check_timers() 551 | 552 | -- Sleep until next timer to wake up, or a set/clear command 553 | -- 554 | local secs 555 | if next_wakeup then 556 | secs = next_wakeup - now_secs() 557 | if secs < 0 then secs = 0 end 558 | end 559 | local key, what = timer_gateway:receive( secs, TGW_KEY, TGW_QUERY) 560 | 561 | if key == TGW_KEY then 562 | assert( getmetatable( what) == "Linda") -- 'what' should be a linda on which the client sets a timer 563 | local _, key, wakeup_at, period = timer_gateway:receive( 0, timer_gateway_batched, TGW_KEY, 3) 564 | assert( key) 565 | set_timer( what, key, wakeup_at, period and period > 0 and period or nil) 566 | elseif key == TGW_QUERY then 567 | if what == "get_timers" then 568 | timer_gateway:send( TGW_REPLY, get_timers()) 569 | else 570 | timer_gateway:send( TGW_REPLY, "unknown query " .. what) 571 | end 572 | --elseif secs == nil then -- got no value while block-waiting? 573 | -- WR( "timer lane: no linda, aborted?") 574 | end 575 | end 576 | end -- timer_body() 577 | timer_lane = gen( "*", { package= {}, priority = max_prio}, timer_body)() -- "*" instead of "io,package" for LuaJIT compatibility... 578 | end -- first_time 579 | 580 | ----- 581 | -- = timer( linda_h, key_val, date_tbl|first_secs [,period_secs] ) 582 | -- 583 | -- PUBLIC LANES API 584 | timer = function( linda, key, a, period ) 585 | if getmetatable( linda) ~= "Linda" then 586 | error "expecting a Linda" 587 | end 588 | if a == 0.0 then 589 | -- Caller expects to get current time stamp in Linda, on return 590 | -- (like the timer had expired instantly); it would be good to set this 591 | -- as late as possible (to give most current time) but also we want it 592 | -- to precede any possible timers that might start striking. 593 | -- 594 | linda:set( key, core.now_secs()) 595 | 596 | if not period or period==0.0 then 597 | timer_gateway:send( TGW_KEY, linda, key, nil, nil ) -- clear the timer 598 | return -- nothing more to do 599 | end 600 | a= period 601 | end 602 | 603 | local wakeup_at= type(a)=="table" and core.wakeup_conv(a) -- given point of time 604 | or (a and core.now_secs()+a or nil) 605 | -- queue to timer 606 | -- 607 | timer_gateway:send( TGW_KEY, linda, key, wakeup_at, period ) 608 | end 609 | 610 | ----- 611 | -- {[{linda, slot, when, period}[,...]]} = timers() 612 | -- 613 | -- PUBLIC LANES API 614 | timers = function() 615 | timer_gateway:send( TGW_QUERY, "get_timers") 616 | local _, r = timer_gateway:receive( TGW_REPLY) 617 | return r 618 | end 619 | 620 | end -- settings.with_timers 621 | 622 | -- avoid pulling the whole core module as upvalue when cancel_error is enough 623 | local cancel_error = assert( core.cancel_error) 624 | 625 | ---=== Lock & atomic generators ===--- 626 | 627 | -- These functions are just surface sugar, but make solutions easier to read. 628 | -- Not many applications should even need explicit locks or atomic counters. 629 | 630 | -- 631 | -- [true [, ...]= trues(uint) 632 | -- 633 | local function trues( n) 634 | if n > 0 then 635 | return true, trues( n - 1) 636 | end 637 | end 638 | 639 | -- 640 | -- lock_f = lanes.genlock( linda_h, key [,N_uint=1] ) 641 | -- 642 | -- = lock_f( +M ) -- acquire M 643 | -- ...locked... 644 | -- = lock_f( -M ) -- release M 645 | -- 646 | -- Returns an access function that allows 'N' simultaneous entries between 647 | -- acquire (+M) and release (-M). For binary locks, use M==1. 648 | -- 649 | -- PUBLIC LANES API 650 | local genlock = function( linda, key, N) 651 | -- clear existing data and set the limit 652 | N = N or 1 653 | if linda:set( key) == cancel_error or linda:limit( key, N) == cancel_error then 654 | return cancel_error 655 | end 656 | 657 | -- use an optimized version for case N == 1 658 | return (N == 1) and 659 | function( M, mode_) 660 | local timeout = (mode_ == "try") and 0 or nil 661 | if M > 0 then 662 | -- 'nil' timeout allows 'key' to be numeric 663 | return linda:send( timeout, key, true) -- suspends until been able to push them 664 | else 665 | local k = linda:receive( nil, key) 666 | -- propagate cancel_error if we got it, else return true or false 667 | return k and ((k ~= cancel_error) and true or k) or false 668 | end 669 | end 670 | or 671 | function( M, mode_) 672 | local timeout = (mode_ == "try") and 0 or nil 673 | if M > 0 then 674 | -- 'nil' timeout allows 'key' to be numeric 675 | return linda:send( timeout, key, trues(M)) -- suspends until been able to push them 676 | else 677 | local k = linda:receive( nil, linda.batched, key, -M) 678 | -- propagate cancel_error if we got it, else return true or false 679 | return k and ((k ~= cancel_error) and true or k) or false 680 | end 681 | end 682 | end 683 | 684 | 685 | -- 686 | -- atomic_f = lanes.genatomic( linda_h, key [,initial_num=0.0]) 687 | -- 688 | -- int|cancel_error = atomic_f( [diff_num = 1.0]) 689 | -- 690 | -- Returns an access function that allows atomic increment/decrement of the 691 | -- number in 'key'. 692 | -- 693 | -- PUBLIC LANES API 694 | local genatomic = function( linda, key, initial_val) 695 | -- clears existing data (also queue). the slot may contain the stored value, and an additional boolean value 696 | if linda:limit( key, 2) == cancel_error or linda:set( key, initial_val or 0.0) == cancel_error then 697 | return cancel_error 698 | end 699 | 700 | return function( diff) 701 | -- 'nil' allows 'key' to be numeric 702 | -- suspends until our 'true' is in 703 | if linda:send( nil, key, true) == cancel_error then 704 | return cancel_error 705 | end 706 | local val = linda:get( key) 707 | if val ~= cancel_error then 708 | val = val + (diff or 1.0) 709 | -- set() releases the lock by emptying queue 710 | if linda:set( key, val) == cancel_error then 711 | val = cancel_error 712 | end 713 | end 714 | return val 715 | end 716 | end 717 | 718 | -- activate full interface 719 | lanes.require = core.require 720 | lanes.register = core.register 721 | lanes.gen = gen 722 | lanes.linda = core.linda 723 | lanes.cancel_error = core.cancel_error 724 | lanes.nameof = core.nameof 725 | lanes.set_singlethreaded = core.set_singlethreaded 726 | lanes.threads = core.threads or function() error "lane tracking is not available" end -- core.threads isn't registered if settings.track_lanes is false 727 | lanes.set_thread_priority = core.set_thread_priority 728 | lanes.set_thread_affinity = core.set_thread_affinity 729 | lanes.timer = timer 730 | lanes.timer_lane = timer_lane 731 | lanes.timers = timers 732 | lanes.sleep = sleep 733 | lanes.genlock = genlock 734 | lanes.now_secs = core.now_secs 735 | lanes.genatomic = genatomic 736 | lanes.configure = nil -- no need to call configure() ever again 737 | return lanes 738 | end -- lanes.configure 739 | 740 | lanesMeta.__index = function( t, k) 741 | -- This is called when some functionality is accessed without calling configure() 742 | lanes.configure() -- initialize with default settings 743 | -- Access the required key 744 | return lanes[k] 745 | end 746 | 747 | -- no need to force calling configure() manually excepted the first time (other times will reuse the internally stored settings of the first call) 748 | _G.lanes = lanes 749 | if core.settings then 750 | return lanes.configure() 751 | else 752 | return lanes 753 | end 754 | 755 | --the end 756 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | gmod_lanes 2 | Modules for Garry's Mod that add threads. 3 | ----------------------------------------------------------------------- 4 | Copyright (c) 2015-2023, Daniel Almeida 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions 9 | are met: 10 | 11 | 1. Redistributions of source code must retain the above copyright 12 | notice, this list of conditions and the following disclaimer. 13 | 14 | 2. Redistributions in binary form must reproduce the above copyright 15 | notice, this list of conditions and the following disclaimer in the 16 | documentation and/or other materials provided with the distribution. 17 | 18 | 3. Neither the name of the copyright holder nor the names of its 19 | contributors may be used to endorse or promote products derived from 20 | this software without specific prior written permission. 21 | 22 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 23 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 25 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 26 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 27 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 28 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 29 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 30 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 31 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 32 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /premake5.lua: -------------------------------------------------------------------------------- 1 | PROJECT_GENERATOR_VERSION = 2 2 | 3 | newoption({ 4 | trigger = "gmcommon", 5 | description = "Sets the path to the garrysmod_common (https://github.com/danielga/garrysmod_common) directory", 6 | value = "path to garrysmod_common directory" 7 | }) 8 | 9 | local gmcommon = assert(_OPTIONS.gmcommon or os.getenv("GARRYSMOD_COMMON"), 10 | "you didn't provide a path to your garrysmod_common (https://github.com/danielga/garrysmod_common) directory") 11 | include(gmcommon) 12 | 13 | local LANES_DIRECTORY = "lanes/src" 14 | 15 | CreateWorkspace({name = "lanes.core"}) 16 | CreateProject({serverside = true}) 17 | includedirs(LANES_DIRECTORY) 18 | links("lanes") 19 | IncludeLuaShared() 20 | 21 | filter("system:linux") 22 | links("pthread") 23 | 24 | CreateProject({serverside = false}) 25 | includedirs(LANES_DIRECTORY) 26 | links("lanes") 27 | IncludeLuaShared() 28 | 29 | filter("system:linux") 30 | links("pthread") 31 | 32 | project("lanes") 33 | kind("StaticLib") 34 | warnings("Default") 35 | includedirs(LANES_DIRECTORY) 36 | externalincludedirs(_GARRYSMOD_COMMON_DIRECTORY .. "/include") 37 | files({path.join(LANES_DIRECTORY, "*.c"), path.join(LANES_DIRECTORY, "*.h")}) 38 | vpaths({ 39 | ["Source files/*"] = path.join(LANES_DIRECTORY, "*.c"), 40 | ["Header files/*"] = path.join(LANES_DIRECTORY, "*.h") 41 | }) 42 | IncludeLuaShared() 43 | 44 | filter("system:linux") 45 | defines("_GNU_SOURCE") 46 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # gmod\_lanes 2 | 3 | Modules for Garry's Mod that add threads through [Lua Lanes][1]. 4 | 5 | ## General information 6 | 7 | This project is composed by the main module (`lanes.lua`) and the internal module (`gm[cl/sv]_lanes_core_[win32/linux/macos].dll`). 8 | You can also refer to this project as gm_lanes or Lanes for Garry's Mod. 9 | `lanes.lua` goes into `lua/includes/modules` and `gm[cl/sv]_lanes_core_[win32/linux/macos].dll` goes into `lua/bin`. 10 | 11 | ## Requirements 12 | 13 | This project requires [garrysmod\_common][2], a framework to facilitate the creation of compilations files (Visual Studio, make, XCode, etc). Simply set the environment variable '**GARRYSMOD\_COMMON**' or the premake option '**gmcommon**' to the path of your local copy of [garrysmod\_common][2]. 14 | 15 | [1]: https://lualanes.github.io/lanes 16 | [2]: https://github.com/danielga/garrysmod_common 17 | -------------------------------------------------------------------------------- /source/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | 3 | extern "C" 4 | { 5 | #include 6 | 7 | int luaopen_io( lua_State * ) 8 | { 9 | return 0; 10 | } 11 | 12 | int luaopen_ffi( lua_State * ) 13 | { 14 | return 0; 15 | } 16 | } 17 | 18 | GMOD_MODULE_OPEN( ) 19 | { 20 | LUA->Pop( 1 ); 21 | LUA->PushString( "lanes.core" ); 22 | 23 | LUA->GetField( GarrysMod::Lua::INDEX_GLOBAL, "lanes" ); 24 | if( luaopen_lanes_core( LUA->GetState( ) ) >= 1 ) 25 | LUA->SetField( -2, "core" ); 26 | 27 | return 0; 28 | } 29 | 30 | GMOD_MODULE_CLOSE( ) 31 | { 32 | LUA->GetField( GarrysMod::Lua::INDEX_GLOBAL, "lanes" ); 33 | LUA->PushNil( ); 34 | LUA->SetField( -2, "core" ); 35 | return 0; 36 | } 37 | --------------------------------------------------------------------------------