Attribution
This Python 201 guide is based on our notes from the TCM Security - Python 201 for Hackers video course by Riley Kidd.
We highly recommend this course as a great starting point for learning Python; it was instrumental in our understanding of the subject. We would like to extend a big thank you to the TCM Security Team for providing such valuable resources!
The Windows API
Introduction
An API (Application Programming Interface) is a mechanism to interface with the Windows operating system. It is mostly described by the Microsoft Developer Network (MSDN), though some functions are not officially documented.
The Windows API is comprised of functions, structures, and constants, including Windows-defined data types. It is not static; it is a dynamic entity that can change and expand between releases. The official implementation lives in DLLs on your Windows machine, such as kernel32.dll and ntdll.dll.
Ctypes enables us to wrap Python around C. Speaking C lets us interface with the Windows API. Some hacker-focused API calls include OpenProcess, CreateRemoteThread, and WriteProcessMemory.
C Data Types and Structures
C Primer
C is a lower-level programming language. It is compiled (and faster), while Python is interpreted (and slower). C requires you to specify data types, has fewer built-in standard functions, and is comparatively more difficult than Python.
Pointers and Structs
When you create a variable, that variable has a memory address. The variable has a value, but it also has a memory address. Pointers are variables that store addresses, not values. Structures (structs) are collections of grouped variables under a single type.
The CTypes Module
The CTypes module is included in the standard library. It is a foreign function library for Python that provides C-compatible data types and allows us to directly call functions in DLLs or shared libraries from Python.
Passing by Reference
Passing by reference occurs when a function expects a pointer to a specific data type as an input parameter. This happens if the function wants to write to that location, or if the data being passed is too large to be passed by its value.
Structures
Structures are like classes. They are derived from the Structure base class defined within the CTypes module. Each subclass must define a fields attribute, which is a list of tuples containing the field name and field type:
_fields_ = [("name", c_char_p), ("age", c_int)]
Lists
Lists in C are like arrays. Setting up the space beforehand is needed. Multiply the data type by the number of elements you want in the array:
person_array_t = PERSON * 3
Demo Example
Feel free to experiment by commenting and uncommenting sections as you see fit.
from ctypes import *
print("-" * 50)
## --------------- C DATA TYPES -- BOOLEAN --------------- ##
# boolean
b0 = c_bool(0)
b1 = c_bool(1)
print(b0)
print(type(b0))
print(b0.value)
print(b1)
print(type(b1))
print(b1.value)
print("-" * 50)
## --------------- C DATA TYPES -- INTEGER --------------- ##
# unsigned integer
i0 = c_uint(-1)
print(i0.value)
print("-" * 50)
## --------------- C DATA TYPES -- STRINGS --------------- #
# strings -- null terminated char pointers
c0 = c_char_p(b"test")
print(c0.value)
# when changing the values of pointer type instances, we are actually changing
# the memory location the variable is pointing to and NOT the actual contents
# of that memory block
print(c0)
c0 = c_char_p(b"test2")
print(c0)
print(c0.value)
print("-" * 50)
## --------------- C DATA TYPES -- STRING BUFFER --------------- #
# reserve a buffer -- changing the value will not change the memory address
# string buffer -- 5 byte buffer -- initialized with null bytes -- \x00
p0 = create_string_buffer(5)
print(p0)
# check the raw memory contents
print(p0.raw)
print(p0.value)
# changing the value at the address
p0.value = b"a"
print(p0.raw)
print(p0.value)
print(p0) # the same memory address
print("-" * 50)
## --------------- C DATA TYPES -- UNICODE BUFFER --------------- #
# unicode buffer is expected --> use unicode buffer
u0 = create_unicode_buffer(5)
print(u0)
print(u0.value)
u0.value = "a"
print(u0)
print(u0.value)
print("-" * 50)
## --------------- C DATA TYPES -- POINTERS -- INTEGER --------------- #
# creating pointers -- pointer function on a c data type
i = c_int(42)
pi = pointer(i)
print(i)
print(pi)
print(pi.contents)
print("-" * 50)
## --------------- C DATA TYPES -- POINTERS -- BUFFER --------------- #
# string buffer
p0 = create_string_buffer(5)
print(p0)
p0.value = b"a"
# check the raw memory contents
print(p0.value)
# print its address in memory
print(hex(addressof(p0)))
print("-" * 50)
## --------------- C DATA TYPES -- REFERENCE --------------- ##
# create a reference to p0
pt = byref(p0)
print(pt)
# cast returns a new instance of type char pointer which points to pt
# which can be used to look at the actual data stored at that memory address
print(cast(pt, c_char_p).value)
# casting as an integer
print(cast(pt, POINTER(c_int)).contents)
print(ord('a'))
print("-" * 50)
## --------------- C DATA TYPES -- STRUCTURES --------------- ##
class PERSON(Structure):
_fields_ = [("name", c_char_p),
("age", c_int)]
bob = PERSON(b"bob", 30)
print(bob.name)
print(bob.age)
# reusing the structure
alice = PERSON(b"alice", 20)
print(alice.name)
print(alice.age)
print("-" * 50)
## --------------- C DATA TYPES -- LISTS/ARRAYS --------------- ##
# setting up space for a list/array
person_array_t = PERSON * 3
print(person_array_t)
# create the person array
person_array = person_array_t()
print(person_array)
# adding people to the array
person_array[0] = PERSON(b"bob", 30)
person_array[1] = PERSON(b"alice", 20)
person_array[2] = PERSON(b"mallory", 50)
for person in person_array:
print(person)
print(person.name)
print(person.age)
Interfacing with the Windows API
The Win32 API often has both ANSI and Unicode versions of a function. Unicode versions have a W appended to the name, while ANSI versions have an A appended. Documentation is always available on MSDN.
Demo Example
Feel free to experiment by commenting and uncommenting sections as you see fit.
from ctypes import *
from ctypes.wintypes import HWND, LPCSTR, UINT, INT, LPSTR, LPDWORD, DWORD, HANDLE, BOOL
print("-" * 80)
## --------------- MESSAGE BOX -- ANSI --------------- ##
# url: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messageboxa
# MessageBox is defined in User32.dll with User32.lib as library
MessageBoxA = windll.user32.MessageBoxA
# define the argument types
MessageBoxA.argtypes = (HWND, LPCSTR, LPCSTR, UINT)
# define the return type
MessageBoxA.restype = INT
# function pointer object
print(MessageBoxA)
# specify the values for the parameters
lpText = LPCSTR(b"World")
lpCaption = LPCSTR(b"Hello")
# type -- MB_OK - one push button (default) -- 0x00000000L -- minus the "L"
MB_OK = 0x00000000
# type -- MB_OKCANCEL - two push buttons -- 0x00000001L -- minus the "L"
MB_OKCANCEL = 0x00000001
# hWnd -- handle -- owner is null --> the box has no owner window
# MessageBoxA(None, lpText, lpCaption, MB_OK)
# MessageBoxA(None, lpText, lpCaption, MB_OKCANCEL)
print("-" * 80)
## --------------- GETUSERNAME -- ANSI --------------- ##
# url: https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getusernamea
GetUserNameA = windll.advapi32.GetUserNameA
GetUserNameA.argtypes = (LPSTR, LPDWORD)
GetUserNameA.restype = INT
# buffer size -- dword = 32 bit unsigned int
# if buffer is not big enough --> nothing is displayed
# buffer_size = DWORD(8)
buffer_size = DWORD(2) # error -- won't fit
# create buffer
buffer = create_string_buffer(buffer_size.value)
# grab the username --
# byref: lpdword is a pointer to a dword
GetUserNameA(buffer, byref(buffer_size))
# display the returned username
print(buffer.value)
print("-" * 80)
## --------------- GETLASTERROR --------------- ##
# identifying the error codes
# 'msdn error codes' -- 122 --> The data area passed to a system call is too small
error = GetLastError()
if error:
print(error)
# the windows error message for this error code
print(WinError(error))
print("-" * 80)
## --------------- WINDOWS SPECIFIC STRUCTURES --------------- ##
# url: https://learn.microsoft.com/en-us/windows/win32/api/windef/ns-windef-rect
# expects a pointer to a rect structure -- LPRECT lpRect
# define the structure RECT
class RECT(Structure):
_fields_ = [("left", c_long),
("top", c_long),
("right", c_long),
("bottom", c_long)]
rect = RECT()
print(rect.left)
print(rect.top)
print(rect.right)
print(rect.bottom)
# changing values
rect.left = 1
print(rect.left)
print("-" * 80)
## --------------- GETWINDOWRECT --------------- ##
# url: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getwindowrect
GetWindowRect = windll.user32.GetWindowRect
# specify argtypes -- handle and pointer
GetWindowRect.argtypes = (HANDLE, POINTER(RECT))
# return type
GetWindowRect.restype = BOOL
# get the handle -- use the GetForegroundWindow function
# retrieves the handle to the foreground window
hwnd = windll.user32.GetForegroundWindow()
# call GetWindowRect
GetWindowRect(hwnd, byref(rect))
# changing the cmd window will change the results
print(rect.left)
print(rect.top)
print(rect.right)
print(rect.bottom)
Undocumented API Calls
Not all Windows APIs are documented on MSDN. The documented APIs we used so far operate in user mode. When calling a user mode API, we eventually end up in kernel mode. Windows APIs are an abstraction layer over the native API. These native API calls are defined in NTDLL.
The native API can be used to evade endpoint detection and response tooling. A number of antivirus and endpoint detection and response solutions use API hooking to gain telemetry for their analysis when trying to identify whether an application’s behavior is potentially malicious.
Getting around this involves using a lower-level API call (like ntdll) which may not be hooked by the security product. The API calls we used so far are userland calls to kernel32. Behind the scenes, they are translated into lower-level API calls in ntdll. APIs in ntdll are mostly undocumented and can change at any time with a new Windows release.
Install Process Hacker to verify memory allocation. It is a more robust version of Task Manager, available at https://processhacker.sourceforge.io/.
Demo Example
Feel free to experiment by commenting and uncommenting sections as you see fit.
from ctypes import *
from ctypes import wintypes
kernel32 = windll.kernel32
# cosmetic -- for using ctypes and windows types interchangeably
SIZE_T = c_size_t
print("-" * 80)
## --------------- VIRTUAL ALLOC --------------- ##
# url: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
# virtualalloc -- tl;dr -> to allocate memory
# reserves, commits, or changes the state of a region of pages in the virtual
# address space of the calling process. Memory allocated by this function is
# automatically initialized to zero
VirtualAlloc = kernel32.VirtualAlloc
# parameters
VirtualAlloc.argtypes = (wintypes.LPVOID, SIZE_T, wintypes.DWORD, wintypes.DWORD)
# return type
VirtualAlloc.restype = wintypes.LPVOID
## --------------- VIRTUAL ALLOC -- PARAMETERS --------------- ##
# lpAddress -- the starting address of the region to allocate
# if NULL -> the system determines where to allocate the region
# dwSize -- the size of the region in bytes
# flAllocationType -- the type of memory allocation
#-- MEM_COMMIT -- allocate memory charges
MEM_COMMIT = 0x00001000
#-- MEM_RESERVE -- reserve a range of the process's virtual address space
# without allocating any actual physical storage in memory or in
# the paging file on disk
MEM_RESERVE = 0x00002000
# flProtect -- the memory protection for the region of pages to be allocated
# constants-url: https://learn.microsoft.com/en-us/windows/win32/Memory/memory-protection-constants
#-- PAGE_EXECUTE_READWRITE -- enables execute, read-only, or read/write access to
# the committed region of pages
PAGE_EXECUTE_READWRITE = 0x40
## --------------- USERLAND -- VIRTUAL ALLOC -- CALLING-IT --------------- ##
# ptr -- base address of the allocated region of pages
ptr = VirtualAlloc(None, 1024 * 4, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
# check for errors
error = GetLastError()
if error:
print(error)
print(WinError(error))
# check where we allocated the memory
print("VirtualAlloc: ", hex(ptr))
# to check it -- use process hacker
# -> filter for 'python' -> open it -> memory -> look for the memory addr
# Type: Private; Size: 4kB; Protection: RWX
# IDEA
# translate the same virtual alloc call, down a layer deeper,
# and directly call the ntdll version of virtual alloc --> ntallocatevirtualmemory
# documented on msdn
print("-" * 80)
## --------------- KERNEL -- NT ALLOCATE VIRTUALMEMORY --------------- ##
# url: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntallocatevirtualmemory
nt = windll.ntdll
NTSTATUS = wintypes.DWORD
# function definition
NtAllocateVirtualMemory = nt.NtAllocateVirtualMemory
NtAllocateVirtualMemory.argtypes = (wintypes.HANDLE, POINTER(wintypes.LPVOID), wintypes.ULONG, POINTER(wintypes.ULONG), wintypes.ULONG, wintypes.ULONG)
NtAllocateVirtualMemory.restype = NTSTATUS
# getting a handle for the current process -> GetCurrentProcess()
handle = 0xffffffffffffffff
base_address = wintypes.LPVOID(0x0)
zero_bits = wintypes.ULONG(0)
size = wintypes.ULONG(1024 * 12)
# allocation type
# protection -- the same as before
# calling the function
ptr2 = NtAllocateVirtualMemory(handle, byref(base_address), zero_bits, byref(size), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
# check if succeeded or not
if ptr2 != 0:
print("error!")
print(ptr2)
print("NtAllocateVirtualMemory: ", hex(base_address.value))
# specify input so that the python process does not die
input()
Direct Syscalls
Assembly Primer
Assembly is a very low-level programming language. It works directly with registers and interrupts. Each line usually translates to one processor instruction. Different processors use different assembly languages.
Syscalls
Every native API call has a specific number that represents it (syscall = system call). Syscall numbers differ between different versions of Windows. Numbers can be verified with a debugger, or you can use public lists. To make a syscall, you need to move the number to a register. In x64, the syscall instruction will then enter kernel mode.
Key Points
With direct syscalls in assembly, we can completely remove any Windows DLL imports. Instead of ntdll doing the work for us, we can set up the call directly ourselves. When making a syscall, we need to set up some arguments on the stack, move the correct syscall for the operation we want to perform into the eax register, then use the syscall CPU instruction. This will cause the syscall to be actually executed in kernel mode.
If you want to use the same functionality as the Win32 API and want to avoid directly interfacing with it entirely from user mode, leverage Python and the CTypes library to directly perform system calls.
Use winver in the Run dialog (Win+R) to verify your current Windows version.
The syscall table by j00ru from Google’s Project Zero is available at https://j00ru.vexillium.org/syscalls/nt/64/.
Executing the System Call: Steps
- Write the ASM as shellcode in order to execute it from within Python.
- Put the shellcode somewhere in memory.
- Update the memory for that location to enable execution (otherwise you will get an error about trying to execute code from a non-executable memory location).
- Call it.
Debugger Software: x64dbg
Download x64dbg from https://x64dbg.com/. To install, extract it and run release > x64 > x96dbg.exe.
Summary
We are able to write assembly into memory, then change the memory protection of that location, define the location as a function, and call that function to directly trigger a syscall. Basically, you can use Python to make a Win32 API call without directly using the Win32 API at all.
Demo Example
Feel free to experiment by commenting and uncommenting sections as you see fit.
from ctypes import *
from ctypes import wintypes
print("-" * 50)
## --------------- DEFINING CONSTANTS --------------- ##
# cosmetic -- for using ctypes and windows types interchangeably
SIZE_T = c_size_t
NTSTATUS = wintypes.DWORD
MEM_COMMIT = 0x00001000
MEM_RESERVE = 0x00002000
PAGE_EXECUTE_READWRITE = 0x40
# NOTE
# - executing the system call -- steps
# - 1 -- write the asm as shellcode in order to execute it from within python
# - 2 -- put the shellcode somewhere in memory
# - 3 -- update the memory for that location to enable execution, otherwise error -- trying to execute code from a non-executable memory location
# - 4 -- call it
## --------------- SYSCALL IN ASM --------------- ##
# system call NtAllocateVirtualMemory -- syscall 0x0018 -- 24 in dec
# -- this is how windows will always execute when we are using the win32 api
"""
mov r10, rcx
mov eax, 18h
syscall
ret
"""
# check if a call succeeded or not
def verify(x):
if not x:
raise WinError()
## --------------- EXAMPLE-FUNCTION -- STEP 1/2 -- SHELLCODE --------------- ##
# NOTE:
# start x64dbg
# -- start a sacrificial process -- notepad -- [win+r] "notepad"
# -- in x64dbg -- File>Attach>Select Notepad
# -- in x64dbg -- right click>binary>fill with nops -- select 80
# -- now we have the space where we can convert our assembly to shellcode
# NOTE:
# typically eax will contain the return value of a function
# -- but it depends on the calling convention
# NOTE:
# testing it -- move the value "5" into eax and then return
# -- if return value is "5" -- we know it's working
# -- in x64dbg -- (6th entry) right click>Assemble> "mov eax, 5; ret"
# -- will convert it into shellcode -- "B8 05000000"
# -- add "ret" to the next line -- "C3"
# corresponds to asm "mov eax, 5; ret"
# buf = create_string_buffer(b"\xb8\x05\x00\x00\x00\xc3")
## change return value from 5 to 3
buf = create_string_buffer(b"\xb8\x03\x00\x00\x00\xc3")
# get the address where it is stored in memory
buf_addr = addressof(buf)
print(hex(buf_addr))
# NOTE:
# let's take a look with process hacker -- check out the memory location
# -- process hacker>search for python> check the memory location
# -- ISSUE-1: the shellcode is not page aligned
# -- ISSUE-2: the memory protection of the page is "RW"
## --- EXAMPLE-FUNCTION -- STEP 3 -- ADJUST MEMORY PAGE PERMISSIONS -- ##
# use the VirtualProtect function
# url: https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect
# define the VirtualProtect function
VirtualProtect = windll.kernel32.VirtualProtect
# define the argument types
VirtualProtect.argtypes = (wintypes.LPVOID, SIZE_T, wintypes.DWORD, wintypes.LPDWORD)
# define the return type
VirtualProtect.restype = wintypes.INT
# function parameters
# -- lpAddress -- starting page of the region of pages -- our buffer address
# -- dwSize -- size of the region -- in bytes
# -- flNewProtect -- new memory protection option
# ---- https://learn.microsoft.com/en-us/windows/win32/Memory/memory-protection-constants
# -- lpflOldProtect -- reference to store the old memory protection
old_protection = wintypes.DWORD(0)
## NOTE: if memory permission is not changed -- OSError: access violating writing <addr>
# calling the function -- change the memory protection
protect = VirtualProtect(buf_addr, len(buf), PAGE_EXECUTE_READWRITE, byref(old_protection))
# check for errors
verify(protect)
# verify it with process hacker that the memory page has the correct permissions
# -- this is where our shellcode is currently stored -- now we can execute it
## --------------- EXAMPLE-FUNCTION -- STEP 4 -- CALLING IT --------------- ##
# use ctypes.cfunctype
# url: https://docs.python.org/3/library/ctypes.html#ctypes.CFUNCTYPE
# expecting number as the return type -- no input parameters
# only expecting 5 as a response
asm_type = CFUNCTYPE(c_int)
# based on the address of our shellcode
asm_function = asm_type(buf_addr)
# calling the function
r = asm_function()
# checking the output
print(hex(r))
print("-" * 50)
## --------------- SYSCALL -- STEP 1/2 -- SHELLCODE --------------- ##
# convert asm function to shellcode -- like before
# "mov r10, rcx" --> 49:89CA -- (in demo:"4c:8bd1")
# "mov eax, 18h" --> B8 18000000
# "syscall" --> 0F05
# "ret" --> C3
# create the buffer to store the shell code
# -- demo version
#buf2 = create_string_buffer(b"\x4c\x8b\xd1\xb8\x18\x00\x00\x00\x0f\x05\xc3")
buf2 = create_string_buffer(b"\x49\x89\xca\xb8\x18\x00\x00\x00\x0f\x05\xc3")
# get the address of where it is stored in memory
buf_addr2 = addressof(buf2)
print("Buffer address:", hex(buf_addr2))
## -- SYSCALL -- STEP 3 -- ADJUST MEMORY PAGE PERMISSIONS -- ##
old_protection = wintypes.DWORD(0)
protect = VirtualProtect(buf_addr2, len(buf2), PAGE_EXECUTE_READWRITE, byref(old_protection))
verify(protect)
## --------------- SYSCALL -- STEP 4 -- CALLING IT --------------- ##
# setting up the function for the syscall as we did with the test asm
# NOTE: the syscall we are emulating is ntallocatevirtualmemory
# url: https://learn.microsoft.com/en-us/windows-hardware/drivers/ddi/ntifs/nf-ntifs-ntallocatevirtualmemory
# return NTstatus and then the arguments
syscall_type = CFUNCTYPE(NTSTATUS, wintypes.HANDLE, POINTER(wintypes.LPVOID), wintypes.ULONG, POINTER(wintypes.ULONG), wintypes.ULONG, wintypes.ULONG)
# set up the function
syscall_function = syscall_type(buf_addr2)
# making the call -- just like in the undocumented api demo
# getting a handle for the current process -> GetCurrentProcess()
handle = 0xffffffffffffffff
base_address = wintypes.LPVOID(0x0)
zero_bits = wintypes.ULONG(0)
# NOTE: change the size from 12kB to 24 for debugging
# size = wintypes.ULONG(1024 * 12)
size = wintypes.ULONG(1024 * 24)
# calling the function
# -- instead of NtAllocateVirtualMemory(...) use syscall_function(...)
# -- with the same parameters
ptr2 = syscall_function(handle, byref(base_address), zero_bits, byref(size), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE)
# verify if it works
if ptr2 != 0:
print("error!")
print(ptr2)
# no errors
print("Syscall allocation: ", hex(base_address.value))
# halt execution
input()
print("-" * 50)
## --------------- END --------------- ##
Execution from a DLL
Windows DLLs: General Overview
Dynamic Link Libraries (DLLs) are similar to executable files. DLL files contain code and data that can be used by multiple programs. They cannot be directly executed, but they can be loaded and used. When loaded, a DLL can specify an entry point, which defines what happens when the DLL is loaded. Alternatively, it can just export functions, which can then be called by the caller. DLL files can be loaded on program startup or during execution. They are not linked or loaded until runtime.
Custom DLL
Here is an example DLL (dll.c) that needs to be compiled with Visual Studio:
#include "pch.h"
#include <stdio.h>
extern "C"
{
__declspec(dllexport) void hello()
{
puts("hello from the dll");
}
__declspec(dllexport) int length(char* input)
{
return strlen(input);
}
__declspec(dllexport) int add(int a, int b)
{
return a + b;
}
__declspec(dllexport) void add_p(int* a, int* b, int* result)
{
*result = *a + *b;
}
};
You can view the exported functions with Dependency Walker, available at https://dependencywalker.com/. To install, download the x64 version, run depends.exe, extract all, and run depends.exe again. To use it, open the compiled DLL.
Demo Example
Feel free to experiment by commenting and uncommenting sections as you see fit.
from ctypes import *
print("-" * 80)
## --------------- Windows Standard C Library -- MSVCRT --------------- ##
# interfacing with the windows standard c library
# -- printing out the time -- pass in None as a null pointer
print(windll.msvcrt.time(None))
# standard c functions -- memset and puts
windll.msvcrt.puts(b"print this!")
mut_str = create_string_buffer(10)
print(mut_str.raw)
# changing the values
mut_str.value = b"AAAAA"
print(mut_str.raw)
# perform similar function with memset
windll.msvcrt.memset(mut_str, c_char(b"X"), 5)
windll.msvcrt.puts(mut_str)
# raw value
print(mut_str.raw)
print("-" * 80)
## -- Interacting with custom DLL -- Calling the functions directly -- ##
# compiled dll
# loading the dll -- need to include the full path
lib = WinDLL("C:\\Users\\user\\Documents\\python201\\Dll.dll")
# calling the external hello function
lib.hello()
print("-" * 80)
## -- Interacting with custom DLL -- Defining hello() -- ##
# -- not required but useful to define the functions -- prototypes
# define the length function from the dll
lib.length.argtypes = (c_char_p, )
lib.length.restype = c_int
# test with str 1
str1 = c_char_p(b"test")
print(lib.length(str1))
# test with str 2
str2 = c_char_p(b"test1234")
print(lib.length(str2))
# Data type differences when interacting with python and c
str3 = b"abc\x00123"
# =7 -- \x00 is 1 byte -- via python
print(len(str3))
# =3 -- strlen counts the bytes up to a null terminator -- via c
print(lib.length(c_char_p(str3)))
print("-" * 80)
## -- Interacting with custom DLL -- Defining add() -- ##
lib.add.argtypes = (c_int, c_int)
lib.add.restype = c_int
# calling it
print("2 + 2 = ", lib.add(2, 2))
# benefit of defining the function -- recognizes type errors
# print("2 + 2 = ", lib.add(2, 2.0)) # float instead of int
print("-" * 80)
## -- Interacting with custom DLL -- Defining add_p() -- ##
lib.add_p.argtypes = (POINTER(c_int), POINTER(c_int), POINTER(c_int))
# no return type -- result pointer is passed as an input
# prepare the variables
x = c_int(2)
y = c_int(4)
result = c_int(0)
# check the values
print(result)
print(result.value)
# calling the function
lib.add_p(byref(x), byref(y), byref(result))
# check the output
print("2 + 4 = ", result.value)
print("-" * 80)
## --------------- END --------------- ##