6

I recently ran into an issue where I see the error "too many C levels (limit is 200)".

What does it mean exactly, and how can I prevent it from happening?

I thought it had to do with circular requires, but it's pointing to a line in my code that creates a new instance of a class, like so:

Class:new()

As for my modules, I did have some circular requires but I attempted to fix them by moving the external class into the same module, like so:

Class.SubClass = Class:new()

Any thoughts?

Update:

Here is an example of what I am running into:

Class.lua

local Class = {}

function Class:new(o)
    o = o or {}
    setmetatable(o, self)
    self.__index = self
    return o
end

return Class

classes/Entity.lua

local Class = require('Class')
local Player = require('classes/Player')

local Entity = Class:new()

function Entity:getPlayer() 
    return Player:new() 
end

return Entity

classes/Player.lua

local Class = require('Class')
local Entity = require('classes/Entity')

local Player = Class:new()

function Player:getEntities() 
    local entities = {}
    for i = 1, 100 do
        entities[i] = Entity:new()
    end
    return entities
end

return Player

I realize that this is a circular dependency, but the only solution I've found is: Lua: How to avoid Circular Requires, which uses globals. Is there a way to avoid this with locals?

idbrii
  • 10,975
  • 5
  • 66
  • 107
eliw00d
  • 321
  • 2
  • 12
  • 3
    No thoughts without seeing your code. Probably, a metamethod invokes itself. Use `rawget` and `rawset` inside metamethods `__index` and `__newindex`. – Egor Skriptunoff Feb 26 '19 at 15:36
  • That would make an amount of sense, given where it's pointing to in the stack trace. I'll do some digging. – eliw00d Feb 26 '19 at 19:15
  • @EgorSkriptunoff I added some example code. – eliw00d Feb 27 '19 at 05:42
  • 1
    Remove the line `local Player = require('classes/Player')` and replace `return Player:new()` with `return require('classes.Player'):new()` – Egor Skriptunoff Feb 27 '19 at 07:35
  • Sounds good. Thank you! Would there be any performance implication for requiring everywhere in that class where I would have used the local Player? – eliw00d Feb 27 '19 at 20:33
  • 1
    Yes. You now have one more function call. Invoking any function in Lua is a costly operation. – Egor Skriptunoff Feb 28 '19 at 07:19
  • @EgorSkriptunoff Makes sense. Do you have any ideas on how to solve this in a performant way? – eliw00d Feb 28 '19 at 15:45
  • 1
    Declare `local Player` but not initialize it. When you needs this value: `if not Player then Player = require('classes/Player') end` Or use dirty trick from [that thread](https://stackoverflow.com/questions/54898054/how-to-avoid-circular-dependency-in-lua-without-global-variables) – Egor Skriptunoff Feb 28 '19 at 16:21

2 Answers2

3

I only know the way to solve that question

too many C levels (limit is 200)

this issue is because of the header include each other, like A require B & B require A

hope it's useful for you :)

Ceobe
  • 31
  • 4
2

First, I'd take a hard look at why Entity:getPlayer needs to exist and why it creates a new player. (Even if that's sample code, why does entity depend on player and not only the other way around?)

Let's look at several ways to solve this problem without global variables.

TL;DR: Follow the "Module split" solution.

Runtime require

This is @Egor's solution. It moves the module import from file loading time to runtime. This hides your dependencies a bit, but it's a minimal change to solve a require loop.

classes/Entity.lua

local Class = require('Class')
local Player

local Entity = Class:new()

function Entity:getPlayer() 
    Player = Player or require('classes/Player')
    return Player:new() 
end

return Entity

Data Injection

Pass the player into the entities that need it.

classes/Entity.lua

local Class = require('Class')
local Player = require('classes/Player')

local Entity = Class:new()

function Entity:init(player_class) 
    self.player_class = player_class
end

function Entity:getPlayer() 
    return self.player_class:new()
end

return Entity

classes/Player.lua

local Class = require('Class')
local Entity = require('classes/Entity')

local Player = Class:new()

function Player:getEntities() 
    local entities = {}
    for i = 1, 100 do
        -- Or instead, we might pass the relevant player instance (self).
        entities[i] = Entity:new(Player)
    end
    return entities
end

return Player

Logic injection

I don't like this solution because it requires that you always require entity.lua with the same path. Lua modules may have different definitions if sometimes you use require('classes/Entity') and other times require('classes.Entity'). This might be a good pattern for partial classes, but that doesn't look like what you're doing. Regardless, listed for completeness.

If you had a global for Entity or another module that owned it, you could do the injection there to solve the path risk.

classes/Player.lua

local Class = require('Class')
local Entity = require('classes/Entity')

local Player = Class:new()

...

-- Adding functionality to an imported module.
function Entity:getPlayer() 
    return Player:new() 
end


return Player

Module split

This is one of the cleanest solutions. When you have two things that depend on each other, make them independent and move the dependency into a higher module. However, it's also the furthest away from your current code. It's likely that you end up using an instance of Population like a global variable, but there's no requirement for it to be global.

classes/Population.lua

local Class = require('Class')
local Entity = require('classes/Entity')
local Player = require('classes/Player')

local Population = Class:new()

function Population:getPlayer() 
    return Player:new() 
end

function Population:getEntities() 
    local entities = {}
    for i = 1, 100 do
        entities[i] = Entity:new()
    end
    return entities
end

return Population
idbrii
  • 10,975
  • 5
  • 66
  • 107