Creating functions through bytecode
Opened this issue · 3 comments
I'm trying to create a new function / lambda purely through bytecode (no actual Python code). I can't really find an example on how to do this.
The bytecode I want to generate:
import bytecode.tests
import dis
dis.dis(bytecode.tests.get_code("def some_fn(x): return x"))
Which prints out:
1 0 LOAD_CONST 0 (<code object some_fn at 0x7fbe893c6b30, file "<string>", line 1>)
2 LOAD_CONST 1 ('some_fn')
4 MAKE_FUNCTION 0
6 STORE_NAME 0 (some_fn)
8 LOAD_CONST 2 (None)
10 RETURN_VALUE
Disassembly of <code object some_fn at 0x7fbe893c6b30, file "<string>", line 1>:
1 0 LOAD_FAST 0 (x)
2 RETURN_VALUE
Here's how I'm trying to create the function:
from bytecode import ConcreteBytecode, ConcreteInstr
# Define the body of the function
bytecode_fn = ConcreteBytecode()
bytecode_fn.varnames = ["x"]
bytecode_fn.extend([ConcreteInstr("LOAD_FAST", 0), # var x
ConcreteInstr("RETURN_VALUE")])
# Convert bytecode_fn to code
fn_code_obj = bytecode_fn.to_code()
bytecode = ConcreteBytecode()
bytecode.names = ["some_fn"]
bytecode.consts = [fn_code_obj, "some_fn", None]
bytecode.extend([ConcreteInstr("LOAD_CONST", 2), # Default x: None
ConcreteInstr("LOAD_CONST", 0), # fn_code_obj
ConcreteInstr("LOAD_CONST", 1), # "some_fn"
ConcreteInstr("MAKE_FUNCTION", 1), # 1 arg
ConcreteInstr("STORE_NAME", 0), # "some_fn"
ConcreteInstr("LOAD_CONST", 2), # None
ConcreteInstr("RETURN_VALUE")])
# Execute bytecode
code = bytecode.to_code()
exec(code)
# Call created function
# Error:
#
# TypeError: <module>() takes from -16 to 0 positional arguments but 1 was given
some_fn(1)
# Error:
#
# UnboundLocalError: local variable 'x' referenced before assignment
some_fn()
This code created the some_fn
function, but with zero arguments. I believe MAKE_FUNCTION requires you to add a default value on TOS (which in my case is None).
How do I adjust this to actually create a function that accepts 1 argument and binds it to x
? If we can figure this out I'd be willing to make a write up and place it in the documentation.
Figured it out. I had to add an argcount
which defaults to 0
.
from bytecode import ConcreteBytecode, ConcreteInstr
bytecode_fn = ConcreteBytecode()
bytecode_fn.varnames = ["x"]
bytecode_fn.consts = [10]
bytecode_fn.argcount = 1
bytecode_fn.extend([ConcreteInstr("LOAD_FAST", 0), # var x
ConcreteInstr("LOAD_CONST", 0),
ConcreteInstr("BINARY_ADD"), # Add 10
ConcreteInstr("RETURN_VALUE")])
fn_code_obj = bytecode_fn.to_code()
bytecode = ConcreteBytecode()
bytecode.names = ["some_fn"]
bytecode.consts = [fn_code_obj, "some_fn", None]
bytecode.extend([ConcreteInstr("LOAD_CONST", 0), # fn_code_obj
ConcreteInstr("LOAD_CONST", 1), # "some_fn"
ConcreteInstr("MAKE_FUNCTION", 0),
ConcreteInstr("STORE_NAME", 0), # "some_fn"
ConcreteInstr("LOAD_CONST", 2), # None
ConcreteInstr("RETURN_VALUE")])
# Execute bytecode
code = bytecode.to_code()
exec(code)
some_fn(1) # Returns 11
I guess I overlooked the fact that ConcreteBytecode
inherits from BaseBytecode
. Would you like this (or similar "howto create a function") in the docs? If so I can make a PR and otherwise this issue can just be closed.
This could be an interesting example to have in the docs. Feel free to make a PR.
Just as a note that one can also create functions with FunctionType
: https://github.com/P403n1x87/surgery/blob/792748fb7497d8e5d0998a54231fc16742250c55/surgery.py#L56