From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from simark.ca by simark.ca with LMTP id OxhIOv8NOWoOPhMAWB0awg (envelope-from ) for ; Mon, 22 Jun 2026 06:27:11 -0400 Authentication-Results: simark.ca; dkim=pass (1024-bit key; secure) header.d=sourceware.org header.i=@sourceware.org header.a=rsa-sha256 header.s=default header.b=FKeplWVV; dkim-atps=neutral Received: by simark.ca (Postfix, from userid 112) id DEAAD1E024; Mon, 22 Jun 2026 06:27:11 -0400 (EDT) X-Spam-Checker-Version: SpamAssassin 4.0.1 (2024-03-25) on simark.ca X-Spam-Level: X-Spam-Status: No, score=-5.4 required=5.0 tests=ARC_SIGNED,ARC_VALID,BAYES_00, DKIM_SIGNED,DKIM_VALID,DKIM_VALID_AU,MAILING_LIST_MULTI, RCVD_IN_DNSWL_MED autolearn=ham autolearn_force=no version=4.0.1 Received: from vm01.sourceware.org (vm01.sourceware.org [IPv6:2620:52:6:3111::32]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature ECDSA (prime256v1) server-digest SHA256) (No client certificate requested) by simark.ca (Postfix) with ESMTPS id EC1DC1E024 for ; Mon, 22 Jun 2026 06:27:08 -0400 (EDT) Received: from vm01.sourceware.org (localhost [IPv6:::1]) by sourceware.org (Postfix) with ESMTP id 1D9474BA2E22 for ; Mon, 22 Jun 2026 10:27:07 +0000 (GMT) DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 1D9474BA2E22 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=sourceware.org; s=default; t=1782124027; bh=4qfNcV9xbNV5mMT/Hrc4Ik9kcey5jcE3nnpy6PBmvSs=; h=Date:Subject:To:Cc:References:In-Reply-To:List-Id: List-Unsubscribe:List-Archive:List-Post:List-Help:List-Subscribe: From:Reply-To:From; b=FKeplWVVi8o0IN9kJy0F9vKQbL9j7PzyloHI8D69ZcY9jB/nmAErWoWBfpcIbkXxA fhP9wTZFXNzUBeOx7zb5JOb0TjcfXyoemV+z2L6UhJ0jIIOH1GQ8R/06oNDDZl4FPY aRFx2v3+fT1FbgogjXaUoxpGXToTPnIS7DDK9/cc= Received: from AM0PR02CU008.outbound.protection.outlook.com (mail-westeuropeazlp170130006.outbound.protection.outlook.com [IPv6:2a01:111:f403:c201::6]) by sourceware.org (Postfix) with ESMTPS id 7526D4BA2E0A for ; Mon, 22 Jun 2026 10:26:12 +0000 (GMT) DMARC-Filter: OpenDMARC Filter v1.4.2 sourceware.org 7526D4BA2E0A ARC-Filter: OpenARC Filter v1.0.0 sourceware.org 7526D4BA2E0A ARC-Seal: i=3; a=rsa-sha256; d=sourceware.org; s=key; t=1782123972; cv=pass; b=I9rgY1oO8g4n4oGzKDUXvMGX/Of3tjuncEe1m1/WfC6RYjWpaFHAsDd7jE9ClgN/o3T+3z4UfyEDKvIb14Y1RNNXMRyOkQ+e1WqGnyhtajjzMQkQDYw4T8iCO98Oi2sTWDjRrSV9KO9/1vl0MldRjOQymgNdm/TeJIZ/gqKJkrg= ARC-Message-Signature: i=3; a=rsa-sha256; d=sourceware.org; s=key; t=1782123972; c=relaxed/simple; bh=127ltyDyiz0QguydcjvPogkCgAH4MPyC8UUjz7LXAbo=; h=DKIM-Signature:DKIM-Signature:Message-ID:Date:Subject:To:From: MIME-Version; b=XC85hKB70COwGN5N4Tk17GhlzeS0F9jkkxtNLqAyM2XsU+aRuLD2uxE2LJqB65qMyaptnFkQFw0MKsS1evRs/09crYB4tq509hPnE2eft32pXaW5/hiaXl/3DL6U1QQh6DpXeX5rvBROzZiQQPk0Cp1i7YLS+7PZSInG4dCk40Y= ARC-Authentication-Results: i=3; sourceware.org; dkim=pass (1024-bit key, unprotected) header.d=arm.com header.i=@arm.com header.a=rsa-sha256 header.s=selector1 header.b=W3qeuNdJ; dkim=pass (1024-bit key) header.d=arm.com header.i=@arm.com header.a=rsa-sha256 header.s=selector1 header.b=W3qeuNdJ DKIM-Filter: OpenDKIM Filter v2.11.0 sourceware.org 7526D4BA2E0A ARC-Seal: i=2; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=pass; b=BRrxvUgy+k4zZf/btzoOQbyjEhkRJVaV5VJjxWF9itjF13GaGUfxFM/M0fDgrVQE3gHMIPnIVdjE0nCovgeOIpY2FCkEf9LSiWEjHlsesD0wu36AhQf1Pj1hX3rq78mAIm1cj2l8o0BP/BBY+Xd3dSz09j/+QWj1Z3Hv9SZ8nYRCaSrAoAtc4pY7a6jZGMd4UUpsSajPWAmRZsc6crmLHjK3K7K2nJMEdqxqXLyyEbx9Wp4I9Jm3ot2ggVg8KKOlT6wBrfXje03jpuB8lvLKCEERkR2c3n8neZvK+1z2ydpvK2r1OCWRf0B9Pul1o+qGE5/5XMF2Y7VE9qMFtvvM0g== ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector10001; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=4qfNcV9xbNV5mMT/Hrc4Ik9kcey5jcE3nnpy6PBmvSs=; b=nGL66Wkl58B/kQlB29db1UNcXEXi9yeVpdbo+ru+BJkru40favapwBRYgkgfdk4vdh4AcA0CxhaJ6l6Bo0hj1Hdmb8RBJFSjjvEvnY45ZU+GwyGhRLsA83rob+IgWctnLT29WJ3LznIwT45QGth4+94i7wzUJBlzkJbXMjpXaerHfJJlNmORdMwKsK5mgQZ9YEPXQdxB1kLQnZO3NvdRmmS68GlrRC0QQBKyWvJRVMMIV0zADXrzpqXIVLXU/nXCI4KSYqA6o8tdyWPuvqZkQXJlZ2+QhazL1gENHuxszkbwdG7KVIwUw/iatB+WRqTMWmLONIXuoe34/QlQZW+C+g== ARC-Authentication-Results: i=2; mx.microsoft.com 1; spf=pass (sender ip is 4.158.2.129) smtp.rcpttodomain=tromey.com smtp.mailfrom=arm.com; dmarc=pass (p=none sp=none pct=100) action=none header.from=arm.com; dkim=pass (signature was verified) header.d=arm.com; arc=pass (0 oda=1 ltdi=1 spf=[1,1,smtp.mailfrom=arm.com] dkim=[1,1,header.d=arm.com] dmarc=[1,1,header.from=arm.com]) Received: from AS4PR09CA0010.eurprd09.prod.outlook.com (2603:10a6:20b:5e0::14) by DU0PR08MB9299.eurprd08.prod.outlook.com (2603:10a6:10:41e::10) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.21.139.19; Mon, 22 Jun 2026 10:25:56 +0000 Received: from AMS1EPF0000008F.eurprd05.prod.outlook.com (2603:10a6:20b:5e0:cafe::1) by AS4PR09CA0010.outlook.office365.com (2603:10a6:20b:5e0::14) with Microsoft SMTP Server (version=TLS1_3, cipher=TLS_AES_256_GCM_SHA384) id 15.21.139.20 via Frontend Transport; Mon, 22 Jun 2026 10:25:56 +0000 X-MS-Exchange-Authentication-Results: spf=pass (sender IP is 4.158.2.129) smtp.mailfrom=arm.com; dkim=pass (signature was verified) header.d=arm.com;dmarc=pass action=none header.from=arm.com; Received-SPF: Pass (protection.outlook.com: domain of arm.com designates 4.158.2.129 as permitted sender) receiver=protection.outlook.com; client-ip=4.158.2.129; helo=outbound-uk1.az.dlp.m.darktrace.com; pr=C Received: from outbound-uk1.az.dlp.m.darktrace.com (4.158.2.129) by AMS1EPF0000008F.mail.protection.outlook.com (10.167.242.86) with Microsoft SMTP Server (version=TLS1_3, cipher=TLS_AES_256_GCM_SHA384) id 15.21.139.8 via Frontend Transport; Mon, 22 Jun 2026 10:25:56 +0000 ARC-Seal: i=1; a=rsa-sha256; s=arcselector10001; d=microsoft.com; cv=none; b=JhxNAeQyYnKHj17zbgNkyVwHDGCaXq2OPFOpIBma9M4LeKcHAmCb7+B2JPBIgBQ45DN1LTFysm4bppYP2va3cv1D7G/wgw49ZsLEZonWIOtFOeFhq+t3NWZOilclH5p86rm/LejCSsrqnixcC8hxH+R9YMJ8YORW9Jd9MN0QkIjFY4dRYgdDWz7zPZqsNnCBRtKhg+wUKy7cBEJrJ58a7jOo45nLs/YEHq4DKu92tXECGPSU0TYvy3Wu9jsn0/Z7OTG0HUq4uR1gHapR6pYudebpID4rpdpR7SO/P5cJ6IpJchWVw9gTIo1/dAPFDyhEtT7B9DK0PuEsUbZDk7NFmQ== ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=microsoft.com; s=arcselector10001; h=From:Date:Subject:Message-ID:Content-Type:MIME-Version:X-MS-Exchange-AntiSpam-MessageData-ChunkCount:X-MS-Exchange-AntiSpam-MessageData-0:X-MS-Exchange-AntiSpam-MessageData-1; bh=4qfNcV9xbNV5mMT/Hrc4Ik9kcey5jcE3nnpy6PBmvSs=; b=N2mOqoLGGpGbsBju8IEGCy3DfGAfYaSzu860mB9fGqYAPdx7JevL8eLCQAIbmDgIvP+TuqmEnBuDBAhEAf3lAbpWHNGywPy39l6A4eUHiKgFnoahAJqib6JUjOfnJDvrZxtfbQKf4+tqhS2HUzDJ9Po9MlyuzW43Lbf+pXmcDSdtAThr0mcRCYjXbcw1OkLSraGyrZRvoobYyiBnFUdWUAO5g26qP/QAiqKEK559w5/Ep367G1AbSMzMnE7AVlvNXU3a8ByiUE4YsSaNphGtUes2VNJrjSLmzBFbBV3MXCbWeYPfYonxDJyJgZ6kHgMBnkbM35n0qu9in/qzrD3ABg== ARC-Authentication-Results: i=1; mx.microsoft.com 1; spf=pass smtp.mailfrom=arm.com; dmarc=pass action=none header.from=arm.com; dkim=pass header.d=arm.com; arc=none Authentication-Results-Original: dkim=none (message not signed) header.d=none;dmarc=none action=none header.from=arm.com; Received: from DU0PR08MB8663.eurprd08.prod.outlook.com (2603:10a6:10:401::6) by PA4PR08MB6144.eurprd08.prod.outlook.com (2603:10a6:102:e3::18) with Microsoft SMTP Server (version=TLS1_2, cipher=TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384) id 15.21.139.19; Mon, 22 Jun 2026 10:24:53 +0000 Received: from DU0PR08MB8663.eurprd08.prod.outlook.com ([fe80::f957:cec9:ec8d:7968]) by DU0PR08MB8663.eurprd08.prod.outlook.com ([fe80::f957:cec9:ec8d:7968%2]) with mapi id 15.21.0139.018; Mon, 22 Jun 2026 10:24:53 +0000 Message-ID: <143b06a7-117f-4500-a911-26f8f79eef2f@arm.com> Date: Mon, 22 Jun 2026 11:24:52 +0100 User-Agent: Mozilla Thunderbird Subject: Re: RFC: prototype of C extensions using the Python limited API To: Tom Tromey , gdb@sourceware.org Cc: Andrew Burgess References: <21479c1d-3a9a-4ae2-bc1a-5dc19871f275@arm.com> Content-Language: en-US In-Reply-To: <21479c1d-3a9a-4ae2-bc1a-5dc19871f275@arm.com> Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit X-ClientProxiedBy: LO4P123CA0607.GBRP123.PROD.OUTLOOK.COM (2603:10a6:600:314::9) To DU0PR08MB8663.eurprd08.prod.outlook.com (2603:10a6:10:401::6) MIME-Version: 1.0 X-MS-TrafficTypeDiagnostic: DU0PR08MB8663:EE_|PA4PR08MB6144:EE_|AMS1EPF0000008F:EE_|DU0PR08MB9299:EE_ X-MS-Office365-Filtering-Correlation-Id: 747f7152-0fa9-47a8-e5a4-08ded048a562 x-checkrecipientrouted: true NoDisclaimer: true X-MS-Exchange-SenderADCheck: 1 X-MS-Exchange-AntiSpam-Relay: 0 X-Microsoft-Antispam-Untrusted: BCL:0; ARA:13230040|23010399003|1800799024|376014|366016|13003099007|18002099003|22082099003|5023799004|11063799006|3023799007|6133799003|56012099006; X-Microsoft-Antispam-Message-Info-Original: aMsw5Yqamo/Bt8id5b2ELAYaP1wxijEi29jvcqXiyNkLs4AOQwcGYQLYfpR8ceK/56ISEghIOMRtYtKCUcDIfu3JmmZiAFdUboUSyPR+EQzNeq9tsrR2omEUx1PkCWI4AN+JIiHVyHCKOTmZNMoN3/3uWJdj3Zj/CynEZoDcnhui6bp0d/NWb0VVjybTwwJQxruE6Eg0xspIHX1qDGJpGklirPBFgJQk5hqtKgpr0sL20KS/2lqxbSOO5hQBVupaSsjuahcSSMedOtK+mJ0bxaJC5qAcQyi8nH+IYaV/CN34dBHe8myXQ0/EK8GjnuwRwmbDd3H7lCzLovSzLhGThNDHzXCmgY/CDDMnJGTBZElvDMFIF7mxpSmIta4/mZVhNsdmSkrxKVkR9YxfMyIvyvK5u251n8rtRsoaKOT5xtovhpbM49DHE0M6GK4aXtLfm9lKXcVrVqhay3ee3ZpCdm61WfNpYnt0lABR6eiwAZQNFSB/x7em6878eFVKp3CHoNN52Fwx4jypbkfz49Nf7cUXgFQLc2IffnY0MAHzk6hwKH8Qh9MJCsxUVngYqFOx8vl9ZWKG5VV3UhFcd7XHaomhCFgCkIEU8h1TP84HUFqlE7lTQkkeSSDRl2wBVeN9l2uprS5gsY6CGKKOgX71z2EJgLZAbn6iD30x070gCBA= X-Forefront-Antispam-Report-Untrusted: CIP:255.255.255.255; CTRY:; LANG:en; SCL:1; SRV:; IPV:NLI; SFV:NSPM; H:DU0PR08MB8663.eurprd08.prod.outlook.com; PTR:; CAT:NONE; SFS:(13230040)(23010399003)(1800799024)(376014)(366016)(13003099007)(18002099003)(22082099003)(5023799004)(11063799006)(3023799007)(6133799003)(56012099006); DIR:OUT; SFP:1101; X-Exchange-RoutingPolicyChecked: f9lYbOE435f2knW5PaZ7p2AJiI54lHWlCTlQ/HVwfb+kbavZ1xQXLyiEm4Cky328ZfinRGjqyAWQ7UXtkaA650upi7+vgCkQ21smASwFkiTTQOy6lcKTPt3Yf0motbor+1vshPgc6VPpxlirrdVHfxbuWBrWOnhY0rRu36fgcq3JOxNir2VZkzy8BH+IKs0SWP1t0ppGqgDoeuRtWQzLXRhEE0ZyCc27NQkjC4s9CqugI1sWO7TKPUwAVZVTffzlpp+wbJPaKXKp9NZ3QAMByGV7tEVME0B5INO6szvuwS7j3WJKtHnqIHUYNe7z9uenssFXgJgYnZBKpn5+EBiHPw== X-MS-Exchange-Transport-CrossTenantHeadersStamped: PA4PR08MB6144 X-EOPAttributedMessage: 0 X-MS-Exchange-Transport-CrossTenantHeadersStripped: AMS1EPF0000008F.eurprd05.prod.outlook.com X-MS-PublicTrafficType: Email X-MS-Office365-Filtering-Correlation-Id-Prvs: 5bab2c43-0651-46cf-ac28-08ded0487f8a X-Microsoft-Antispam: BCL:0; ARA:13230040|1800799024|82310400026|36860700016|23010399003|376014|14060799003|35042699022|13003099007|3023799007|6133799003|18002099003|22082099003|56012099006|11063799006|5023799004; X-Microsoft-Antispam-Message-Info: gqBPsnmdS9dqMMwnOeya3JYSjGk2Fjvwu0+Vd4zDRueamknYs3URgtzOjQX/DJoPRrNl30jPnyGTK7V6Fph9R7c5D6tSwETfKqz7ACcMEPscYmgrBaQmNH8hAxQsGyaudKQKgVtw9mxiBH7smg7OhRAWQLU39cYbrk/MDRR8yySUw0Xpl5l0/h5tEleSpUxl4NjmPbBH3MMN4OFMfC3IkSZFb+bBZDWWK9yDbJd/3RhXzEvBuneDact5WAfyTldNfheKg0cT5BGFL/aPL1Ri6g1/K1t5UyCTVHe3ZV4TLj+lzuIb1LU0c5DheWzhcAvLUpKlHL6ud/ctk8axOKJ3Itli2er9oVFHX4DSJlKpuzlvLLdM9PfgpELdstEc26sty6hBvWqc7m0z10bhu1Ff3NE/hQM7n7+4qXfg9stgXxF6vWrC+XIpbGEQVb41ZOlAiNEN/Ope0vw8ptpBQkIvHmpnwczH9ZrRfM77QAwG7LvkwEZlsPu+vX+FuIDhJUBbZamaxlgoIWCqOZTEnCCo5RlEgDCZuVWFrk2JO18EZrioz6H6nlbMklr8zGf3mNcS8LPWaV/aHA6QhtUPU7rUUj4YQ8VNXjaEieuC3qvjBQ+eCav2KciU0FNaT/jAB6T7Nd+jrewmvGWBHEQNf3oJKjSH1BKAWENQv7PkmT8FZeoeodwlBuLZee0LwHnXjgboRGcF4+1V/2lo/F59tRyJxA== X-Forefront-Antispam-Report: CIP:4.158.2.129; CTRY:GB; LANG:en; SCL:1; SRV:; IPV:NLI; SFV:NSPM; H:outbound-uk1.az.dlp.m.darktrace.com; PTR:InfoDomainNonexistent; CAT:NONE; SFS:(13230040)(1800799024)(82310400026)(36860700016)(23010399003)(376014)(14060799003)(35042699022)(13003099007)(3023799007)(6133799003)(18002099003)(22082099003)(56012099006)(11063799006)(5023799004); DIR:OUT; SFP:1101; X-MS-Exchange-AntiSpam-MessageData-ChunkCount: 1 X-MS-Exchange-AntiSpam-MessageData-0: CdVtA4/nsZSsmdRCAN4IM2DM9vzxAg0ihxfHbedgkCC9/3yhvGNF5g+Kj2mrClC8OcHod7HfogFAT/FR8V8z5AeqQvCwqRQWqyOA8OoTKrbQ42k+Q95q+unZfsjmzHRq48ex/wpojEwsJZrKphZgl4AMqOY5WL2Lj4NwNdqvLAbi42aWOIY/1ooQQg+QKs9Yl4udDpSPA/wbUeXzf6kykrHaotbr4NNLwM3Y1YeKuuRyn9WV3RV5hdvHO6Oqs2yy7yfdm+VCh94f3TKhSQeToreqYDcyvYlwtvD5rNKThl/Oy3Awwl7S3/v1YGTRM00bPLakeQN2hHlexGOD8Y0JR4hiHybQvmgeNo983HR0vV7cpB9bUUBLkKvIP4V8GcGrVb7WG97OvxPqDzC9GCP00E1AnyxGMvRgamBFUy4YBR4f78hyZWwzYys/VKWB5m6Q X-OriginatorOrg: arm.com X-MS-Exchange-CrossTenant-OriginalArrivalTime: 22 Jun 2026 10:25:56.3152 (UTC) X-MS-Exchange-CrossTenant-Network-Message-Id: 747f7152-0fa9-47a8-e5a4-08ded048a562 X-MS-Exchange-CrossTenant-Id: f34e5979-57d9-4aaa-ad4d-b122a662184d X-MS-Exchange-CrossTenant-OriginalAttributedTenantConnectingIp: TenantId=f34e5979-57d9-4aaa-ad4d-b122a662184d; Ip=[4.158.2.129]; Helo=[outbound-uk1.az.dlp.m.darktrace.com] X-MS-Exchange-CrossTenant-AuthSource: AMS1EPF0000008F.eurprd05.prod.outlook.com X-MS-Exchange-CrossTenant-AuthAs: Anonymous X-MS-Exchange-CrossTenant-FromEntityHeader: HybridOnPrem X-MS-Exchange-Transport-CrossTenantHeadersStamped: DU0PR08MB9299 X-BeenThere: gdb@sourceware.org X-Mailman-Version: 2.1.30 Precedence: list List-Id: Gdb mailing list List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , From: Matthieu Longo via Gdb Reply-To: Matthieu Longo Errors-To: gdb-bounces~public-inbox=simark.ca@sourceware.org Sender: "Gdb" On 28/05/2026 17:24, Matthieu Longo via Gdb wrote: > Hi Tom, > > As I previously told you (at least, I believe so but I am not 100% sure), I finished migrating all > the Python C extensions some time ago. I had discovered some issues with a case of inheritance > causing unexpected crashes. > To facilitate the debugging, I decided to implement a prototype outside of GDB, and managed to make > it work. I finally got the prototype reviewed by Victor Stinner, a Python core developer, to double- > check whether my design was working as expected. > > Please find attached my last iteration. I also provided an archive which should be more convenient > for you if you want to compile and test it. > > Could you please review it so that we validate this new approach before I migrate all my previous > patches to it ? > > PS: > - a README is provided with all the needed instruction inside. > - this patch contains code from outside GDB's repository, and should not be merged inside GDB's > repository. > > Regards, > Matthieu This might be easier to review. Matthieu >From a5bb99c2df2ab85dc335f33ae5e06b624a4e7df2 Mon Sep 17 00:00:00 2001 From: Matthieu Longo Date: Mon, 22 Jun 2026 10:36:17 +0100 Subject: [PATCH] first commit --- CMakeLists.txt | 57 +++++ README.md | 65 ++++++ include/gdb-others.hpp | 64 ++++++ include/gdb_ref_ptr.hpp | 301 +++++++++++++++++++++++++ include/py-obj-dict.hpp | 189 ++++++++++++++++ include/py-obj-type.hpp | 424 ++++++++++++++++++++++++++++++++++++ include/py-ref.hpp | 156 +++++++++++++ include/python-internal.hpp | 14 ++ include/python-traits.hpp | 93 ++++++++ pydbg.cpp | 80 +++++++ src/A.cpp | 113 ++++++++++ src/B.cpp | 116 ++++++++++ src/ObjWithAlloc.cpp | 140 ++++++++++++ src/gdb-others.cpp | 31 +++ src/my_py_module.cpp | 29 +++ src/my_py_module.hpp | 45 ++++ src/py-obj-type.cpp | 80 +++++++ src/python-internal.cpp | 87 ++++++++ 18 files changed, 2084 insertions(+) create mode 100644 CMakeLists.txt create mode 100644 README.md create mode 100644 include/gdb-others.hpp create mode 100644 include/gdb_ref_ptr.hpp create mode 100644 include/py-obj-dict.hpp create mode 100644 include/py-obj-type.hpp create mode 100644 include/py-ref.hpp create mode 100644 include/python-internal.hpp create mode 100644 include/python-traits.hpp create mode 100644 pydbg.cpp create mode 100644 src/A.cpp create mode 100644 src/B.cpp create mode 100644 src/ObjWithAlloc.cpp create mode 100644 src/gdb-others.cpp create mode 100644 src/my_py_module.cpp create mode 100644 src/my_py_module.hpp create mode 100644 src/py-obj-type.cpp create mode 100644 src/python-internal.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..f524e72 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,57 @@ +cmake_minimum_required(VERSION 4.3) +project(PythonCppExtension) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +find_package(Python3 "3.14" COMPONENTS Interpreter Development) + +set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fdiagnostics-color=always") +set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fdiagnostics-color=always") + +#set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fsanitize=address -fsanitize=leak") +#set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fsanitize=address -fsanitize=leak") + +set(SRCS + "src/gdb-others.cpp" + "src/py-obj-type.cpp" + "src/python-internal.cpp" + "src/my_py_module.cpp" + "src/A.cpp" + "src/B.cpp" + "src/ObjWithAlloc.cpp") +add_library(MyModule SHARED ${SRCS}) +set_target_properties( + MyModule + PROPERTIES + PREFIX "" + OUTPUT_NAME "MyModule" + LINKER_LANGUAGE C + LINK_LIBRARIES Python3::Python + INCLUDE_DIRECTORIES "${PYTHON_INCLUDE_DIR};${CMAKE_CURRENT_SOURCE_DIR}/include;${CMAKE_CURRENT_SOURCE_DIR}/src" + COMPILE_DEFINITIONS "Py_LIMITED_API=0x030e0000" + ) + +add_executable(PyDebug "pydbg.cpp") +target_link_libraries(PyDebug ${PYTHON_LIBRARIES}) +set_target_properties( + PyDebug + PROPERTIES + PREFIX "" + OUTPUT_NAME "PyDebug" + LINKER_LANGUAGE C + LINK_LIBRARIES Python3::Python + INCLUDE_DIRECTORIES "${PYTHON_INCLUDE_DIR};${CMAKE_CURRENT_SOURCE_DIR}" + ) + +add_custom_target(test-pydebug + COMMAND cmake -E env PYTHONPATH=${CMAKE_BINARY_DIR} ${CMAKE_BINARY_DIR}/PyDebug 2>&1 + VERBATIM) +add_dependencies(test-pydebug PyDebug) + +add_custom_target(test-py + COMMAND cmake -E env PYTHONPATH=${CMAKE_BINARY_DIR} python3 ${CMAKE_CURRENT_SOURCE_DIR}/test.py + VERBATIM) +add_dependencies(test-py MyModule) + +add_subdirectory(victor_stinner) diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc9fafe --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +## Build + +```bash +cmake -G Ninja -DCMAKE_BUILD_TYPE=Debug -B build-dir +``` + +## Testing + +```bash +cd build-dir +ninja && ninja test-pydebug +# OR to get the error message that is swalled by CMake +PYTHONPATH=$(pwd) ./PyDebug +``` + +### Expected output + +```text +['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__', '__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__static_attributes__', '__str__', '__subclasshook__', '__weakref__', '_cycle', 'foo', 'iama', 'iamb', 'name'] +B Hello I am A Hello I am both A and B +Calling E::tp_traverse_impl +--> B_traverse +E: calling MyModule.A::tp_traverse +Calling E::tp_traverse_impl +--> A_traverse +visiting __dict__ +E: calling Py_VISIT (Py_TYPE (self)) +E: skipping Py_VISIT (Py_TYPE (self)), will call it in MyModule.A +Calling E::tp_traverse_impl +--> B_traverse +E: calling MyModule.A::tp_traverse +Calling E::tp_traverse_impl +--> A_traverse +visiting __dict__ +E: calling Py_VISIT (Py_TYPE (self)) +E: skipping Py_VISIT (Py_TYPE (self)), will call it in MyModule.A +Calling (null)::tp_clear_impl +--> B_clear +(null): calling MyModule.A::tp_clear +Unexcepted error occurred (TO INVESTIGATE) +AttributeError: __module__ +Calling (null)::tp_clear_impl +--> A_clear +clearing __dict__ +Exception ignored in tp_clear of E: +AttributeError: __module__ +Calling (null)::tp_dealloc +Unexcepted error occurred (TO INVESTIGATE) +AttributeError: __module__ +Calling (null)::tp_clear_impl +--> B_clear +(null): calling MyModule.A::tp_clear +Unexcepted error occurred (TO INVESTIGATE) +AttributeError: __module__ +Calling (null)::tp_clear_impl +--> A_clear +clearing __dict__ +Deleting type (null) +``` + +### Issues + +1. Not sure why A_clear and B_clear are called twice. Is it due to the cycle ? +2. The type name is `(null)` because `PyType_GetFullyQualifiedName()` returns `NULL` for an unknown reason. Please could you explain me why ? +3. There is an unexpected error `AttributeError: __module__`. I don't understand where it comes from. diff --git a/include/gdb-others.hpp b/include/gdb-others.hpp new file mode 100644 index 0000000..36798b3 --- /dev/null +++ b/include/gdb-others.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include +#include + +extern "C" { + +#define ATTRIBUTE_UNUSED_RESULT __attribute__ ((__warn_unused_result__)) +#define ATTRIBUTE_USED __attribute__ ((__used__)) +#define ATTRIBUTE_UNUSED __attribute__((unused)) + +#define gdb_assert(expr) \ + ((void) ((expr) ? 0 : \ + (gdb_assert_fail (#expr, __FILE__, __LINE__, __func__), 0))) + +/* This prints an "Assertion failed" message, asking the user if they + want to continue, dump core, or just exit. */ +#define gdb_assert_fail(assertion, file, line, function) \ + internal_error_loc (file, line, "%s: Assertion `%s' failed.", \ + function, assertion) + +/* The canonical form of gdb_assert (0). + MESSAGE is a string to include in the error message. */ + +#define gdb_assert_not_reached(message, ...) \ + internal_error_loc (__FILE__, __LINE__, _("%s: " message), __func__, \ + ##__VA_ARGS__) + +[[noreturn]] extern void internal_error_loc (const char *file, int line, + const char *fmt, ...) + __attribute__ ((format (printf, 3, 4)));; + +} + + +/* Wrap PyGetSetDef to allow convenient construction with string + literals. Unfortunately, PyGetSetDef's 'name' and 'doc' members + are 'char *' instead of 'const char *', meaning that in order to + list-initialize PyGetSetDef arrays with string literals (and + without the wrapping below) would require writing explicit 'char *' + casts. Instead, we extend PyGetSetDef and add constexpr + constructors that accept const 'name' and 'doc', hiding the ugly + casts here in a single place. */ + +struct gdb_PyGetSetDef : PyGetSetDef +{ + constexpr gdb_PyGetSetDef (const char *name_, getter get_, setter set_, + const char *doc_, void *closure_) + : PyGetSetDef {const_cast (name_), get_, set_, + const_cast (doc_), closure_} + {} + + /* Alternative constructor that allows omitting the closure in list + initialization. */ + constexpr gdb_PyGetSetDef (const char *name_, getter get_, setter set_, + const char *doc_) + : gdb_PyGetSetDef {name_, get_, set_, doc_, NULL} + {} + + /* Constructor for the sentinel entries. */ + constexpr gdb_PyGetSetDef (std::nullptr_t) + : gdb_PyGetSetDef {NULL, NULL, NULL, NULL, NULL} + {} +}; diff --git a/include/gdb_ref_ptr.hpp b/include/gdb_ref_ptr.hpp new file mode 100644 index 0000000..379850f --- /dev/null +++ b/include/gdb_ref_ptr.hpp @@ -0,0 +1,301 @@ +/* Reference-counted smart pointer class + + Copyright (C) 2016-2026 Free Software Foundation, Inc. + + This file is part of GDB. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#ifndef GDBSUPPORT_GDB_REF_PTR_H +#define GDBSUPPORT_GDB_REF_PTR_H + +#include + +struct gdbpy_ref_policy; + +namespace gdb +{ + +/* An instance of this class either holds a reference to a + reference-counted object or is "NULL". Reference counting is + handled externally by a policy class. If the object holds a + reference, then when the object is destroyed, the reference is + decref'd. + + Normally an instance is constructed using a pointer. This sort of + initialization lets this class manage the lifetime of that + reference. + + Assignment and copy construction will make a new reference as + appropriate. Assignment from a plain pointer is disallowed to + avoid confusion about whether this acquires a new reference; + instead use the "reset" method -- which, like the pointer + constructor, transfers ownership. + + The policy class must provide two static methods: + void incref (T *); + void decref (T *); +*/ +template +class ref_ptr +{ +public: + + /* Befriend all instantiations of this template, so that the + templated copy constructors and assignment operators can access + the data. */ + template friend class ref_ptr; + + /* Create a new NULL instance. */ + ref_ptr () noexcept + : m_obj (NULL) + { + } + + /* Create a new NULL instance. Note that this is not explicit. */ + ref_ptr (const std::nullptr_t) noexcept + : m_obj (NULL) + { + } + + /* Create a new instance. OBJ is a reference, management of which + is now transferred to this class. */ + explicit ref_ptr (T *obj) noexcept + : m_obj (obj) + { + } + + /* Copy another instance. */ + template> + ref_ptr (const ref_ptr &other) + : m_obj (other.m_obj) + { + if (m_obj != NULL) + Policy::incref (m_obj); + } + + ref_ptr (const ref_ptr &other) + : m_obj (other.m_obj) + { + if (m_obj != NULL) + Policy::incref (m_obj); + } + + /* Transfer ownership from OTHER. */ + template> + ref_ptr (ref_ptr &&other) noexcept + : m_obj (other.m_obj) + { + other.m_obj = NULL; + } + + ref_ptr (ref_ptr &&other) noexcept + : m_obj (other.m_obj) + { + other.m_obj = NULL; + } + + /* Destroy this instance. */ + ~ref_ptr () + { + if (m_obj != NULL) + Policy::decref (m_obj); + } + + /* Copy another instance. */ + template> + ref_ptr &operator= (const ref_ptr &other) + { + /* Note that self-assignment is not checked here, as it isn't + possible: self-assignments will choose the non-template + function. */ + reset (other.m_obj); + if (m_obj != NULL) + Policy::incref (m_obj); + return *this; + } + + ref_ptr &operator= (const ref_ptr &other) + { + /* Do nothing on self-assignment. */ + if (this != &other) + { + reset (other.m_obj); + if (m_obj != NULL) + Policy::incref (m_obj); + } + return *this; + } + + /* Transfer ownership from OTHER. */ + template> + ref_ptr &operator= (ref_ptr &&other) + { + /* Note that self-assignment is not checked here, as it isn't + possible: self-assignments will choose the non-template + function. */ + reset (other.m_obj); + other.m_obj = NULL; + return *this; + } + + ref_ptr &operator= (ref_ptr &&other) + { + /* Do nothing on self-assignment. */ + if (this != &other) + { + reset (other.m_obj); + other.m_obj = NULL; + } + return *this; + } + + /* Change this instance's referent. OBJ is a reference, management + of which is now transferred to this class. */ + void reset (T *obj) + { + if (m_obj != NULL) + Policy::decref (m_obj); + m_obj = obj; + } + + operator bool () const noexcept + { + return m_obj != nullptr; + } + + /* Return this instance's referent without changing the state of + this class. */ + T *get () const noexcept + { + return m_obj; + } + + /* Return this instance's referent, and stop managing this + reference. The caller is now responsible for the ownership of + the reference. */ + ATTRIBUTE_UNUSED_RESULT T *release () noexcept + { + T *result = m_obj; + + m_obj = NULL; + return result; + } + + /* Let users refer to members of the underlying pointer. */ + T *operator-> () const noexcept + { + return m_obj; + } + + /* Acquire a new reference and return a ref_ptr that owns it. */ + template + static ref_ptr new_reference (TObj *obj) + { + Policy::incref (obj); + if constexpr (std::is_base_of::value + || std::is_base_of::value) + return ref_ptr (static_cast (obj)); + else + return ref_ptr (obj); + } + + template::value>> + int visit (typename P::visitproc visit, void *arg) noexcept + { + return P::visit (m_obj, visit, arg); + } + + template::value>> + void clear (P * = nullptr) noexcept + { + P::clear (&m_obj); + } + + private: + + T *m_obj; +}; + +template +inline bool operator== (const ref_ptr &lhs, + const ref_ptr &rhs) +{ + return lhs.get () == rhs.get (); +} + +template +inline bool operator== (const ref_ptr &lhs, const U *rhs) +{ + return lhs.get () == rhs; +} + +template +inline bool operator== (const ref_ptr &lhs, const std::nullptr_t) +{ + return lhs.get () == nullptr; +} + +template +inline bool operator== (const T *lhs, const ref_ptr &rhs) +{ + return lhs == rhs.get (); +} + +template +inline bool operator== (const std::nullptr_t, const ref_ptr &rhs) +{ + return nullptr == rhs.get (); +} + +template +inline bool operator!= (const ref_ptr &lhs, + const ref_ptr &rhs) +{ + return lhs.get () != rhs.get (); +} + +template +inline bool operator!= (const ref_ptr &lhs, const U *rhs) +{ + return lhs.get () != rhs; +} + +template +inline bool operator!= (const ref_ptr &lhs, const std::nullptr_t) +{ + return lhs.get () != nullptr; +} + +template +inline bool operator!= (const T *lhs, const ref_ptr &rhs) +{ + return lhs != rhs.get (); +} + +template +inline bool operator!= (const std::nullptr_t, const ref_ptr &rhs) +{ + return nullptr != rhs.get (); +} + +} + +#endif /* GDBSUPPORT_GDB_REF_PTR_H */ diff --git a/include/py-obj-dict.hpp b/include/py-obj-dict.hpp new file mode 100644 index 0000000..42154ba --- /dev/null +++ b/include/py-obj-dict.hpp @@ -0,0 +1,189 @@ +#pragma once + +#include "py-obj-type.hpp" + +/* Helpers for Python extension objects that have a __dict__ attribute. + + Any Python C object extension (for instance, a class A inheriting PyObject) + needing __dict__ should own an attribute GDBPYOBJECT_DICT_ATTR (A). + + Access to the dict requires a custom getter defined via PyGetSetDef. + gdb_PyGetSetDef my_object_getset[] = + { + gdbpy_dict_attr_getter (A, "object"), + ... + { nullptr } + }; + + It is also important to note that __dict__ is used during the attribute + look-up. Since this dictionary is not managed by Python and is not exposed + via tp_dictoffset, custom attribute getter (tp_getattro) and setter + (tp_setattro) are required to correctly redirect attribute access to the + dictionary: + PyType_Slot object_type_slot[] = { + ..., + gdbpy_dict_attr_PyType_Slots (A), + ... + }; + + Finally, heap-allocated types owning a __dict__ should visit and clear their + __dict__. + static int + A_tp_traverse (PyObject *self, visitproc visit, void *arg) + { + auto *obj = (A *) self; + gdbpy_dict_attr_Py_VISIT (obj); + ... + return 0; + } + + static void + A_tp_clear (PyObject *self) + { + auto *obj = (A *) self; + gdbpy_dict_attr_Py_CLEAR (obj); + ... + } + */ +template +struct gdbpy_dict_helpers +{ +private: + + /* Compute the address of the __dict__ attribute for the given PyObject. */ + static PyObject **compute_addr (PyObject *self) + { + auto *dict_owner = reinterpret_cast (self); + return &dict_owner->dict; + } + +public: + + static PyObject * + allocate (PyObject *self) + { + PyObject **py_dict_ptr = gdbpy_dict_helpers::compute_addr (self); + PyObject *py_dict = *py_dict_ptr; + gdb_assert (py_dict == nullptr); + *py_dict_ptr = PyDict_New (); + py_dict = *py_dict_ptr; + return py_dict; + } + + + /* Generic implementation of the getter for the __dict__ attribute for objects + having a dictionary. The CLOSURE argument is unused. */ + + static PyObject * + getter (PyObject *self, void *closure ATTRIBUTE_UNUSED) + { + PyObject **py_dict_ptr = gdbpy_dict_helpers::compute_addr (self); + PyObject *py_dict = *py_dict_ptr; + if (py_dict == nullptr) + { + PyErr_SetString (PyExc_AttributeError, + "This object has no __dict__"); + return nullptr; + } + //if (py_dict == nullptr) + // py_dict = gdbpy_dict_helpers::allocate (self, py_dict_ptr); + return Py_NewRef (py_dict); + } + + /* Generic attribute getter function similar to PyObject_GenericGetAttr () but + that should be used when the object has a dictionary __dict__. */ + static PyObject * + getattro (PyObject *self, PyObject *attr) + { + PyObject *value = PyObject_GenericGetAttr (self, attr); + if (value != nullptr) + return value; + + if (! PyErr_ExceptionMatches (PyExc_AttributeError)) + return nullptr; + + gdbpy_ref<> dict (gdbpy_dict_helpers::getter (self, nullptr)); + if (dict == nullptr) + return nullptr; + + /* Clear previous AttributeError set by PyObject_GenericGetAttr when it + did not find the attribute, and try to get the attribute from __dict__. */ + PyErr_Clear(); + + value = PyDict_GetItemWithError (dict.get (), attr); + if (value != nullptr) + return Py_NewRef (value); + + /* If PyDict_GetItemWithError() returns NULL because an error occurred, it + sets an exception. Propagate it by returning NULL. */ + if (PyErr_Occurred () != nullptr) + return nullptr; + + /* If the key is not found, PyDict_GetItemWithError() returns NULL without + setting an exception. Failing to set one here would later result in: + : error return without exception set + Therefore, we must explicitly raise an AttributeError in this case. */ + PyErr_Format (PyExc_AttributeError, + "'%s' object has no attribute '%s'", + gdbpy_py_obj_tp_name (self), + PyUnicode_AsUTF8AndSize (attr, nullptr)); + return nullptr; + } + + /* Generic attribute setter function similar to PyObject_GenericSetAttr () but + that should be used when the object has a dictionary __dict__. */ + static int + setattro (PyObject *self, PyObject *attr, PyObject *value) + { + if (PyObject_GenericSetAttr (self, attr, value) == 0) + return 0; + + if (! PyErr_ExceptionMatches (PyExc_AttributeError)) + return -1; + + gdbpy_ref<> dict (gdbpy_dict_helpers::getter (self, nullptr)); + if (dict == nullptr) + return -1; + + /* Clear previous AttributeError set by PyObject_GenericGetAttr() when it + did not find the attribute, and try to set the attribute into __dict__. */ + PyErr_Clear(); + + /* Set the new value. + Note: the old value is managed by PyDict_SetItem(), so no need to get + a borrowed reference on it and decrement its reference counter before + setting a new value. */ + return PyDict_SetItem (dict.get (), attr, value); + } +}; + +#define GDBPYOBJECT_DICT_ATTR(O) \ + PyObject *dict; \ + using dict_attr = gdbpy_dict_helpers + +#define gdbpy_dict_attr_getter(O, object_name) \ + { \ + "__dict__", /* name */ \ + (getter) O::dict_attr::getter, \ + (setter) nullptr, \ + "The __dict__ for this " object_name ".", /* doc */ \ + nullptr, /* closure */ \ + } + +#define gdbpy_dict_attr_PyType_Slots(O) \ + {Py_tp_getattro, reinterpret_cast(O::dict_attr::getattro)}, \ + {Py_tp_setattro, reinterpret_cast(O::dict_attr::setattro)} + +#define gdbpy_dict_attr_Py_ALLOC(T, self)\ + gdbpy_dict_helpers::allocate (self) + +#define gdbpy_dict_attr_Py_VISIT(obj) \ + printf ("visiting __dict__\n"); \ + Py_VISIT (obj->dict) + +#define gdbpy_dict_attr_Py_CLEAR(obj) \ + printf ("clearing __dict__\n"); \ + Py_CLEAR (obj->dict) + +#define gdbpy_dict_attr_Py_ALLOC(T, self) \ + gdbpy_dict_helpers::allocate (self) diff --git a/include/py-obj-type.hpp b/include/py-obj-type.hpp new file mode 100644 index 0000000..0762519 --- /dev/null +++ b/include/py-obj-type.hpp @@ -0,0 +1,424 @@ +/* Helpers related to Python object type + + Copyright (C) 2026 Free Software Foundation, Inc. + + This file is part of GDB. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#ifndef GDB_PYTHON_PY_OBJ_TYPE_H +#define GDB_PYTHON_PY_OBJ_TYPE_H + +#include "py-ref.hpp" +#include +#include + +/* Return the type's fully qualified name from a PyTypeObject. */ +extern std::string gdb_py_tp_name (PyTypeObject *py_type) noexcept; + +/* Return the type's fully qualified name from a PyObject. */ +extern std::string gdbpy_py_obj_tp_name (PyObject *self) noexcept; + +using gdbpy_bref_pytype = gdbpy_borrowed_ref; + +namespace gdbpy { + template + struct is_pyobject_wrapper: std::false_type {}; + + template + struct is_pyobject_wrapper>: std::true_type {}; + + template + struct is_pyobject_wrapper>: std::true_type {}; + + template + constexpr bool is_pyobject_wrapper_v = is_pyobject_wrapper::value; +} // namespace gdbpy + +template +struct gdbpy_object_type +{ +private: + PyTypeObject *pytype_ () const noexcept + { + return ((TypeWrapper *) this)->pytype (); + } + +public: + + operator PyTypeObject *() const noexcept + { + return this->pytype_ (); + } + + std::string tp_name () const noexcept + { + return gdb_py_tp_name (this->pytype_ ()); + } + + bool operator== (const TypeWrapper &other) const noexcept + { + return this->pytype_ () == other.pytype_ (); + } + + bool same_as (PyTypeObject *objtype) const noexcept + { + return objtype == this->pytype_ (); + } + + bool same_as (PyObject *obj) const noexcept + { + /* Note: if the type objects are the same, their address should be + the same because GDB manages all those types as singletons. */ + return (obj != nullptr + && ((PyType_Check (obj) != 0 + && this->same_as (reinterpret_cast (obj))) + || this->same_as (Py_TYPE (obj)))) + || this->pytype_ () == nullptr; + } + + bool same_as (std::nullptr_t) const noexcept + { + return this->pytype_ () == nullptr; + } + + bool same_as (const gdbpy_borrowed_ref &wrapper) const noexcept + { + return this->pytype_ () == wrapper.get (); + } + + template + bool same_as (const gdbpy_ref &wrapper) const noexcept + { + return this->same_as (static_cast (wrapper.get ())); + } + + void *slot (int slot_id) const noexcept + { + return PyType_GetSlot (this->pytype_ (), slot_id); + } + + gdbpy_borrowed_ref base_type () const noexcept + { + return gdbpy_borrowed_ref + ((PyTypeObject *) this->slot (Py_tp_base)); + } + + bool heap_type () const noexcept + { + return PyType_GetFlags (this->pytype_ ()) | Py_TPFLAGS_HEAPTYPE; + } +}; + +struct gdbpy_object_type_ref: + public gdbpy_object_type , + public gdbpy_ref <> +{ + gdbpy_object_type_ref () noexcept = default; + gdbpy_object_type_ref (gdbpy_object_type_ref &&other) noexcept = default; + gdbpy_object_type_ref& operator= (gdbpy_object_type_ref &&other) noexcept = default; + + gdbpy_object_type_ref (std::nullptr_t) noexcept + : gdbpy_ref<> (nullptr) + { + } + + gdbpy_object_type_ref (gdbpy_ref <> &&other) noexcept + : gdbpy_ref<> (std::move(other)) + { + gdb_assert (PyType_Check (this->get ()) != 0); + } + + PyTypeObject *pytype () const noexcept + { + return reinterpret_cast (this->get ()); + } +}; + +struct gdbpy_object_type_bref: + public gdbpy_object_type , + public gdbpy_borrowed_ref <> +{ + gdbpy_object_type_bref () noexcept = default; + gdbpy_object_type_bref (const gdbpy_object_type_bref &other) noexcept = default; + gdbpy_object_type_bref& operator= (const gdbpy_object_type_bref &other) noexcept = default; + + gdbpy_object_type_bref (const gdbpy_object_type_ref& other) noexcept + : gdbpy_borrowed_ref<> (other.get ()) + { + } + + gdbpy_object_type_bref (std::nullptr_t) noexcept + : gdbpy_borrowed_ref<> (nullptr) + { + } + + PyTypeObject *pytype () const noexcept + { + if (this->get () == nullptr) + return nullptr; + return reinterpret_cast (this->get ()); + } +}; + +struct gdbpy_py_obj_type_bref: + public gdbpy_object_type , + public gdbpy_borrowed_ref +{ + gdbpy_py_obj_type_bref () noexcept = default; + gdbpy_py_obj_type_bref (const gdbpy_py_obj_type_bref &other) noexcept = default; + gdbpy_py_obj_type_bref& operator= (gdbpy_py_obj_type_bref &&other) noexcept = default; + + gdbpy_py_obj_type_bref (const gdbpy_borrowed_ref &pytype) noexcept + : gdbpy_borrowed_ref (pytype) + { + } + + gdbpy_py_obj_type_bref (PyTypeObject *pytype) noexcept + : gdbpy_borrowed_ref (pytype) + { + } + + PyTypeObject *pytype () const noexcept + { + return this->get (); + } +}; + +/* Wrapper around PyObject_TypeCheck(). + Note: this wrapper will be decommissioned once all the Python extension + types will have been migrated to the Python limited API. */ +inline bool +gdbpy_type_check (PyObject *obj, const PyTypeObject &obj_type) +{ + return PyObject_TypeCheck (obj, const_cast (&obj_type)); +} + +/* Wrapper around PyObject_TypeCheck() to compare against any wrapper + convertible to 'PyTypeObject *'. */ +template > +inline bool +gdbpy_type_check (PyObject *obj, const OType &obj_type) +{ + return PyObject_TypeCheck (obj, obj_type.pytype ()); +} + +/* Wrapper around PyObject_TypeCheck() similar to the previous one, but + accepting 'gdbpy_ref' and 'gdbpy_borrowed_ref' instead of raw + 'PyObject *'. */ +template , + typename = std::is_convertible> +inline bool +gdbpy_type_check (const Wrapper &obj, const OType &obj_type) +{ + return PyObject_TypeCheck (obj.get (), obj_type.pytype ()); +} + +struct gdbpy_heap_type +{ + using tp_traverse_t = int (*) (PyObject *self, visitproc visit, void *arg); + using tp_clear_t = void (*) (PyObject *self); + + static constexpr unsigned long + tp_flags_default () + { return Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC; } + + static int + tp_traverse (PyObject *self, visitproc visit, void *arg, + const gdbpy_object_type_bref &base_type, + tp_traverse_t tp_traverse_impl) + { + int res = 0; + + /* Traverse the object's attributes. */ + if (tp_traverse_impl != nullptr) + { + if ((res = tp_traverse_impl (self, visit, arg)) != 0) + return res; + } + + /* Don't forget to call tp_traverse of the base class. */ + if (base_type != nullptr) + { + auto base_tp_traverse + = (gdbpy_heap_type::tp_traverse_t) base_type.slot (Py_tp_traverse); + if ((res = base_tp_traverse (self, visit, arg)) != 0) + return res; + } + + /* From Python 3.9, tp_traverse must visit the type for heap-allocated + types. Older versions must not visit it. + Note that, in case of inheritance, the type should be visited only + once. */ +#if PY_VERSION_HEX >= 0x03090000 + if (base_type == nullptr) + { + printf ("calling Py_VISIT (Py_TYPE (self))\n"); + Py_VISIT (Py_TYPE (self)); + } +#endif + + return res; + } + + static void + tp_clear (PyObject *self, + const gdbpy_object_type_bref &base_type, + tp_clear_t tp_clear_impl) + { + /* Clear any fields specific to the type. */ + if (tp_clear_impl != nullptr) + tp_clear_impl (self); + + /* Don't forget to call tp_clear of the base class. */ + if (base_type != nullptr) + { + auto base_tp_clear + = (gdbpy_heap_type::tp_clear_t) base_type.slot (Py_tp_clear); + base_tp_clear (self); + } + } + + static void + tp_dealloc (PyObject *self) + { + auto *tp = Py_TYPE (self); + + /* Call PyObject_GC_UnTrack() before any fields are invalidated. */ + PyObject_GC_UnTrack (self); + + /* Clear the object's attributes. */ + auto self_tp_clear = (tp_clear_t) PyType_GetSlot (tp, Py_tp_clear); + if (self_tp_clear) + self_tp_clear (self); + + /* Delete the object. */ + PyObject_GC_Del (self); + + /* Decrement the reference count of the type. */ + printf ("decrement ref count on Py_TYPE (self)\n"); + Py_DECREF (tp); + } + + /* A convenient wrapper enforcing the call to PyObject_GC_Track() on a newly + allocated Python object before it goes out of scope. If a developer + forgets to call track(), the destructor raises a runtime assertion in the + destructor. */ + template + struct auto_gc_track + : private gdbpy_ref + { + auto_gc_track (T *obj) + : gdbpy_ref (obj) + {} + + auto_gc_track (auto_gc_track &&other) + : gdbpy_ref (std::move (other)) + {} + + auto_gc_track &operator= (auto_gc_track &&other) + { + if (this != other) + gdbpy_ref::operator= (std::move (other)); + return *this; + } + + gdbpy_borrowed_ref borrowed_ref () const noexcept + { + return gdbpy_ref::get (); + } + + gdbpy_ref strong_ref () const noexcept + { + return gdbpy_ref(*this); + } + + /* Start tracking of the newly allocated object with the GC. + Return gdbpy_ref as the finalized object. */ + gdbpy_ref track () + { + PyObject_GC_Track (this->get ()); + return std::move (*this); + } + + /* Bail out, i.e. free the newly allocated object and return NULL. */ + std::nullptr_t bailout () + { + auto obj = this->track (); + return nullptr; + } + + inline explicit operator bool () const noexcept + { + return gdbpy_ref::get () != nullptr; + } + + inline bool operator== (const std::nullptr_t) const noexcept + { + return gdbpy_ref::get () == nullptr; + } + + T *operator-> () const noexcept + { + return gdbpy_ref::get (); + } + + ~auto_gc_track () + { + /* The object should have been tracked by the GC, and released before + this destructor is called. */ + gdb_assert (gdbpy_ref::get () == nullptr); + } + }; + + /* Convenient wrapper to PyObject_GC_New() returning an auto_gc_track + object, used to enforce the call to PyObject_GC_Track() on the newly + allocated Python object before auto_gc_track goes out of scope. */ + template + static auto_gc_track + make_pyobj (const gdbpy_object_type_bref &obj_type) + { + auto_gc_track obj + (static_cast (PyObject_GC_New (T, obj_type.pytype ()))); + return obj; + } +}; + +/* Helper macro to initialize mandatory slot IDs (i.e. Py_tp_traverse, + Py_tp_clear, and Py_tp_dealloc) for garbage-collected types, using + default methods. + Note: those defaults are ideal for classes containing no strong + references of Python objects in their attributes. */ + +/* Helper macro to initialize mandatory slot IDs (i.e. Py_tp_traverse, + Py_tp_clear, and Py_tp_dealloc) for garbage-collected types, providing + custom methods for tp_traverse and tp_clear. */ +#define gdbpy_heap_type_methods(fn_traverse, fn_clear) \ + {Py_tp_traverse, (void *) fn_traverse}, \ + {Py_tp_clear, (void *) fn_clear}, \ + {Py_tp_dealloc, (void *) gdbpy_heap_type::tp_dealloc} + +/* Helper macro declaring 'varname', creating a new heap-allocated object of + static type OBJTYPE, initializing it following specs declared in py_objtype + (the dynamically-allocated type corresponding to OBJTYPE), and validating + its creation or bailing out. */ +#define gdbpy_heap_type_allocate_or_bailout(varname, OBJTYPE, py_objtype)\ + auto varname = gdbpy_heap_type::make_pyobj (py_objtype); \ + if (varname == nullptr) \ + return nullptr + +#endif /* GDB_PYTHON_PY_OBJ_TYPE_H */ diff --git a/include/py-ref.hpp b/include/py-ref.hpp new file mode 100644 index 0000000..f254977 --- /dev/null +++ b/include/py-ref.hpp @@ -0,0 +1,156 @@ +/* Python reference-holding class + + Copyright (C) 2016-2026 Free Software Foundation, Inc. + + This file is part of GDB. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#ifndef GDB_PYTHON_PY_REF_H +#define GDB_PYTHON_PY_REF_H + +#include "gdb_ref_ptr.hpp" +#include +#include "python-traits.hpp" + +/* A policy class for gdb::ref_ptr for Python reference counting. */ +struct gdbpy_ref_policy +{ + static void incref (PyObject *ptr) + { + Py_INCREF (ptr); + } + + static void decref (PyObject *ptr) + { + Py_DECREF (ptr); + } +}; + +/* A gdb::ref_ptr that has been specialized for Python objects or + their "subclasses". */ +template using gdbpy_ref + = gdb::ref_ptr; + +/* A class representing a borrowed reference. + + This is a simple wrapper for a PyObject*. Aside from documenting + what the code does, the main advantage of using this is that + conversion to a gdbpy_ref<> is guaranteed to make a new + reference. */ +template +class gdbpy_borrowed_ref +{ +public: + + gdbpy_borrowed_ref () noexcept + : m_obj (nullptr) + { + } + + gdbpy_borrowed_ref (const std::nullptr_t) noexcept + : m_obj (nullptr) + { + } + + template > + gdbpy_borrowed_ref (U *obj) noexcept + : m_obj (obj) + { + } + + template > + gdbpy_borrowed_ref (const gdbpy_ref &ref) noexcept + : m_obj (ref.get ()) + { + } + + gdbpy_borrowed_ref (const gdbpy_borrowed_ref &other) noexcept + : m_obj (other.m_obj) + { + } + + gdbpy_borrowed_ref &operator= (const gdbpy_borrowed_ref &other) + { + m_obj = other.m_obj; + return *this; + } + + operator bool () const noexcept + { + return m_obj != nullptr; + } + + operator T * () const noexcept + { + return m_obj; + } + + /* When converting a borrowed reference to a gdbpy_ref<>, a new + reference is acquired. */ + template > + operator gdbpy_ref () + { + gdb_assert (m_obj != nullptr); + return gdbpy_ref::new_reference (m_obj); + } + + gdbpy_ref strong_ref () const noexcept + { + return static_cast> (*this); + } + + /* Let users refer to members of the underlying pointer. */ + T *operator-> () const noexcept + { + return m_obj; + } + + T *get () const noexcept + { + return m_obj; + } + +private: + T *m_obj; +}; + +template +inline bool operator== (const gdbpy_borrowed_ref &lhs, const std::nullptr_t) +{ + return lhs.get () == nullptr; +} + +template +inline bool operator!= (const gdbpy_borrowed_ref &lhs, const std::nullptr_t) +{ + return lhs.get () != nullptr; +} + +template +inline bool operator== (const std::nullptr_t, const gdbpy_borrowed_ref &rhs) +{ + return nullptr == rhs.get (); +} + +template +inline bool operator!= (const std::nullptr_t, const gdbpy_borrowed_ref &rhs) +{ + return nullptr != rhs.get (); +} + +#endif /* GDB_PYTHON_PY_REF_H */ diff --git a/include/python-internal.hpp b/include/python-internal.hpp new file mode 100644 index 0000000..151ad47 --- /dev/null +++ b/include/python-internal.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include "gdb-others.hpp" +#include "py-ref.hpp" +#include "py-obj-dict.hpp" +#include "py-obj-type.hpp" + +extern gdbpy_object_type_ref +gdbpy_type_initialize (gdbpy_borrowed_ref<> &mod, + PyType_Spec *spec, + PyObject *bases = nullptr, + bool disallow_instantiation = false); diff --git a/include/python-traits.hpp b/include/python-traits.hpp new file mode 100644 index 0000000..d1d3053 --- /dev/null +++ b/include/python-traits.hpp @@ -0,0 +1,93 @@ +/* Type traits relating to GDB's Python integration. + + Copyright (C) 2008-2026 Free Software Foundation, Inc. + + This file is part of GDB. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#ifndef GDB_PYTHON_PYTHON_TRAITS_H +#define GDB_PYTHON_PYTHON_TRAITS_H + +namespace gdb +{ +/* All of our custom Python types are created as structs, like this: + + struct some_new_type : public PyObject + { + ... various fields ... + }; + + Then instances of this struct are created by calling PyObject_New, + either directly within GDB's C++ code, or within Python when a user's + Python script creates an instance of that class. + + The problem is that Python is written in C, and PyObject_New doesn't + call any constructors for `some_new_type`, nor for any of the fields + within `some_new_type`. + + If `some_new_type` is Plain Old Data (POD), then this is fine. Or, to + be more C++ specific, if `some_new_type` is trivially default + constructable, then we're fine. + + But if a field within `some_new_type` has a non-trivial constructor, + then we're in trouble as that constructor will never be run. + + An example of a problematic field type is frame_info_ptr. The + constructor for this type registers the new object with a central + management object, recording the `this` pointer, using this type within + `some_new_type` will not work as expected; frame invalidation will not + show up within the frame_info_ptr as you might expect. + + And so, this type trait exists. Whenever a struct is created to define + a new Python type we should add a line like: + + static_assert (gdb::is_python_allocatable_v); + + This will fail if any field of `some_new_type` is unsuitable for this + use. + + We don't actually check is_trivially_default_constructible here. Some + types, e.g. ui_file_style::color, have non-trivial (or no default) + constructors, but are still safe to use within `some_new_type` because + their constructors just initialise data fields; there's nothing + "special" that the constructor does that cannot be achieved by + assigning the fields after creation with PyObject_New. + + What actually matters is that the type is trivially destructible + (Python won't call C++ destructors, so destructors with side effects, + like deregistering from a list, would be skipped) and trivially + copyable (Python may copy objects with memcpy). Types like + frame_info_ptr, whose constructors and destructors have side effects + such as registering with a central management object, will be caught + because they are neither trivially destructible nor trivially copyable. + Types like ui_file_style are trivially destructible and copyable, so + pass this trait. */ + +template +struct is_python_allocatable +{ + static constexpr bool value = + std::is_trivially_destructible_v + && std::is_trivially_copyable_v; +}; + +/* Helper for the above trait to make it more usable. */ + +template +inline constexpr bool is_python_allocatable_v + = is_python_allocatable::value; +} + +#endif /* GDB_PYTHON_PYTHON_TRAITS_H */ diff --git a/pydbg.cpp b/pydbg.cpp new file mode 100644 index 0000000..bcfae2e --- /dev/null +++ b/pydbg.cpp @@ -0,0 +1,80 @@ +#include +#include + +#include + +int main(int argc, char *argv[], char *envp[]) +{ + Py_Initialize(); + + PyObject *pmodule = PyImport_ImportModule("MyModule"); + if (!pmodule) { + PyErr_Print(); + std::cerr << "Failed to import module MyModule" << std::endl; + return -1; + } + + PyObject *classA = PyObject_GetAttrString(pmodule, "A"); + if (!classA) { + std::cerr << "Unable to get type A from MyModule" << std::endl; + return -1; + } + + PyObject *classB = PyObject_GetAttrString(pmodule, "B"); + if (!classB) { + std::cerr << "Unable to get type B from MyModule" << std::endl; + return -1; + } + + PyObject *myAInstance = PyObject_CallObject(classA, NULL); + if (!myAInstance) { + std::cerr << "Instantioation of MyClass failed" << std::endl; + return -1; + } + Py_DECREF(myAInstance); + + PyObject *pGlobal = PyDict_New(); + PyObject *pValue = PyRun_String( + "import MyModule as MM\n" + "b = MM.B()\n" + "b.foo = 1234\n" + "print(b.name(), b.iamb())\n" + "\n" + "class C(MM.B):\n" + " pass\n" + "c = C()\n" + "c.foo = 1234\n" + "print(dir(c))\n" + "print(c.name(), c.iamb())\n" + "\n" + "class D(MM.A):\n" + " def __init__(self):\n" + " super().__init__()\n" + " self._cycle = self\n" + "\n" + "d = D()\n" + "d.foo = 1234\n" + "print(dir(d))\n" + "print(d.name(), d.iama())\n" + "\n" + "class E(MM.B):\n" + " def __init__(self):\n" + " super().__init__()\n" + " self._cycle = self\n" + "\n" + "e = E()\n" + "e.foo = 1234\n" + "print(dir(e))\n" + "print(e.name(), e.iama(), e.iamb())\n" + "\n" + "f = MM.ObjWithAlloc()\n" + "print('{} {}'.format(f.name(), f.whoami()))\n" + "print(f.speak())\n" + , Py_file_input, pGlobal, pGlobal); + Py_XDECREF(pValue); + Py_DECREF(pGlobal); + + Py_Finalize(); + + return 0; +} diff --git a/src/A.cpp b/src/A.cpp new file mode 100644 index 0000000..c2738a5 --- /dev/null +++ b/src/A.cpp @@ -0,0 +1,113 @@ +#include "my_py_module.hpp" + +namespace { + extern PyType_Spec A_object_type_spec; +} /* namespace anonymous */ + +namespace { + +static int +A_init (PyObject *self, PyObject *args, PyObject *kwds) +{ + auto* obj = (A *) self; + gdbpy_dict_attr_Py_ALLOC (A, self); + obj->m_a = PyUnicode_FromString ("A"); + return 0; +} + +/* The tp_traverse callback for the A type. */ +static int +A_traverse_impl (PyObject *self, visitproc visit, void *arg) +{ + auto *obj = (A *) self; + printf ("--> A_traverse_impl\n"); + gdbpy_dict_attr_Py_VISIT (obj); + Py_VISIT (obj->m_a); + return 0; +} + +static int +A_traverse (PyObject *self, visitproc visit, void *arg) +{ + return gdbpy_heap_type::tp_traverse (self, visit, arg, + nullptr, A_traverse_impl); +} + +/* The tp_clear callback for the A type. */ + +static void +A_clear_impl (PyObject *self) +{ + auto *obj = (A *) self; + printf ("--> A_clear_impl\n"); + gdbpy_dict_attr_Py_CLEAR (obj); + Py_CLEAR (obj->m_a); +} + +static void +A_clear (PyObject *self) +{ + return gdbpy_heap_type::tp_clear (self, nullptr, A_clear_impl); +} + +PyObject * +A_name (PyObject *self, PyObject *args) +{ + auto *obj = (A *) self; + Py_INCREF (obj->m_a); + return obj->m_a; +} + +PyObject * +A_iama (PyObject *self, PyObject *args) +{ + auto *obj = (A *) self; + return PyUnicode_FromFormat ("Hello I am %U", obj->m_a); +} + +gdb_PyGetSetDef A_object_getset[] = +{ + gdbpy_dict_attr_getter (A, "A"), + { nullptr } /* Sentinel */ +}; + +PyMethodDef A_object_methods [] = { + { "name", A_name, METH_NOARGS, + "name () -> String.\n\ +Return the name of the class as a string value." }, + { "iama", A_iama, METH_NOARGS, + "iama () -> String.\n\ +Return the object's greeting as a string value." }, + {nullptr} /* Sentinel */ +}; + +PyType_Slot A_object_type_slot[] = { + {Py_tp_doc, const_cast("A object")}, + gdbpy_heap_type_methods (A_traverse, A_clear), + gdbpy_dict_attr_PyType_Slots (A), + {Py_tp_init, (void*) A_init}, + {Py_tp_methods, A_object_methods}, + {Py_tp_getset, A_object_getset}, + {0, nullptr}, /* Sentinel */ +}; + +PyType_Spec A_object_type_spec = { + "MyModule.A", /* tp_name */ + sizeof (A), /* tp_basicsize */ + 0, /* tp_itemsize */ + gdbpy_heap_type::tp_flags_default () + | Py_TPFLAGS_BASETYPE, /* tp_flags */ + A_object_type_slot, +}; + +} // namespace anonymous + +gdbpy_object_type_ref +A::record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class ATTRIBUTE_UNUSED) +{ + gdbpy_object_type_ref obj_type + = gdbpy_type_initialize (module, + &A_object_type_spec); + return obj_type; +} diff --git a/src/B.cpp b/src/B.cpp new file mode 100644 index 0000000..4dfd3a9 --- /dev/null +++ b/src/B.cpp @@ -0,0 +1,116 @@ +#include "my_py_module.hpp" + +namespace { + extern PyType_Spec B_object_type_spec; + gdbpy_object_type_bref B_base_type = nullptr; +} /* namespace anonymous */ + +namespace { + +static int +B_init(PyObject *self, PyObject *args, PyObject *kwds) +{ + auto* obj = (B *) self; + + /* super().__init__(*args, **kwargs) */ + initproc base_func_init = (initproc) B_base_type.slot (Py_tp_init); + gdb_assert (base_func_init != nullptr); + if (base_func_init (self, args, kwds) != 0) + return -1; + + obj->m_b = PyUnicode_FromString ("B"); + return 0; +} + +/* The tp_traverse callback for the A type. */ +static int +B_traverse_impl (PyObject *self, visitproc visit, void *arg) +{ + auto *obj = (B *) self; + printf ("--> B_traverse_impl\n"); + Py_VISIT (obj->m_b); + return 0; +} + +static int +B_traverse(PyObject *self, visitproc visit, void *arg) +{ + return gdbpy_heap_type::tp_traverse(self, visit, arg, + B_base_type, B_traverse_impl); +} + +/* The tp_clear callback for the A type. */ + +static void +B_clear_impl (PyObject *self) +{ + auto *obj = (B *) self; + printf ("--> B_clear_impl\n"); + Py_CLEAR (obj->m_b); +} + +static void +B_clear (PyObject *self) +{ + gdbpy_heap_type::tp_clear(self, B_base_type, B_clear_impl); +} + +PyObject * +B_name (PyObject *self, PyObject *args) +{ + auto *obj = (B *) self; + gdb_assert (obj->m_b != nullptr); + Py_INCREF (obj->m_b); + return obj->m_b; +} + +PyObject * +B_iamb (PyObject *self, PyObject *args) +{ + auto *obj = (B *) self; + gdb_assert (obj->m_a != nullptr); + gdb_assert (obj->m_b != nullptr); + return PyUnicode_FromFormat ("Hello I am both %U and %U", obj->m_a, obj->m_b); +} + +PyMethodDef B_object_methods [] = { + { "name", B_name, METH_NOARGS, + "name () -> String.\n\ +Return the name of the class as a string value." }, + { "iamb", B_iamb, METH_NOARGS, + "iamb () -> String.\n\ +Return the object's greeting as a string value." }, + {nullptr} /* Sentinel */ +}; + +PyType_Slot B_object_type_slot[] = { + {Py_tp_doc, const_cast("B object")}, + gdbpy_heap_type_methods (B_traverse, B_clear), + {Py_tp_init, (void*) B_init}, + {Py_tp_methods, B_object_methods}, + {0, nullptr}, /* Sentinel */ +}; + +PyType_Spec B_object_type_spec = { + "MyModule.B", /* tp_name */ + sizeof (B), /* tp_basicsize */ + 0, /* tp_itemsize */ + gdbpy_heap_type::tp_flags_default () + | Py_TPFLAGS_BASETYPE, /* tp_flags */ + B_object_type_slot, +}; + +} // namespace anonymous + +gdbpy_object_type_ref +B::record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref A_base_class) +{ + B_base_type = A_base_class; + + gdbpy_object_type_ref obj_type + = gdbpy_type_initialize (module, + &B_object_type_spec, + A_base_class.get ()); + return obj_type; +} diff --git a/src/ObjWithAlloc.cpp b/src/ObjWithAlloc.cpp new file mode 100644 index 0000000..ed0b377 --- /dev/null +++ b/src/ObjWithAlloc.cpp @@ -0,0 +1,140 @@ +#include "my_py_module.hpp" + +namespace { + extern PyType_Spec ObjWithAlloc_object_type_spec; + gdbpy_object_type_bref ObjWithAlloc_object_type; +} /* namespace anonymous */ + +namespace { + +static PyObject * +ObjWithAlloc_alloc () +{ + gdbpy_heap_type_allocate_or_bailout + (my_new_obj, ObjWithAlloc, ObjWithAlloc_object_type); + + my_new_obj->msg = new std::string ( + "Neque porro quisquam est qui dolorem ipsum quia dolor sit amet, " + "consectetur, adipisci velit..."); + + my_new_obj->m_name = PyUnicode_FromString ("ObjWithAlloc"); + + my_new_obj->self_cycle = my_new_obj.strong_ref ().release (); + + /* This is only an example to illustrate the usage of bailout(). */ + if (*my_new_obj->msg == "hello world !") + { + delete my_new_obj->msg; + Py_XDECREF (my_new_obj->m_name); + Py_XDECREF (my_new_obj->self_cycle); + return my_new_obj.bailout (); + } + return my_new_obj.track ().release (); +} + +/* The tp_traverse callback for the A type. */ +static int +ObjWithAlloc_traverse_impl (PyObject *self, visitproc visit, void *arg) +{ + auto *obj = (ObjWithAlloc *) self; + printf ("--> ObjWithAlloc_traverse_impl\n"); + Py_VISIT (obj->m_name); + Py_VISIT (obj->self_cycle); + return 0; +} + +static int +ObjWithAlloc_traverse (PyObject *self, visitproc visit, void *arg) +{ + return gdbpy_heap_type::tp_traverse (self, visit, arg, + nullptr, ObjWithAlloc_traverse_impl); +} + +/* The tp_clear callback for the A type. */ + +static void +ObjWithAlloc_clear_impl (PyObject *self) +{ + auto *obj = (ObjWithAlloc *) self; + printf ("--> ObjWithAlloc_clear_impl\n"); + Py_CLEAR (obj->m_name); + Py_CLEAR (obj->self_cycle); + if (obj->msg != nullptr) + { + delete obj->msg; + obj->msg = nullptr; + } +} + +static void +ObjWithAlloc_clear (PyObject *self) +{ + return gdbpy_heap_type::tp_clear (self, nullptr, ObjWithAlloc_clear_impl); +} + +PyObject * +ObjWithAlloc_name (PyObject *self, PyObject *args) +{ + auto *obj = (ObjWithAlloc *) self; + Py_INCREF (obj->m_name); + return obj->m_name; +} + +PyObject * +ObjWithAlloc_whoami (PyObject *self, PyObject *args) +{ + auto *obj = (ObjWithAlloc *) self; + return PyUnicode_FromFormat ("Hello I am %U", obj->m_name); +} + +PyObject * +ObjWithAlloc_speak (PyObject *self, PyObject *args) +{ + auto *obj = (ObjWithAlloc *) self; + return PyUnicode_FromFormat ("I, %U, %s", obj->m_name, obj->msg->c_str ()); +} + +PyMethodDef ObjWithAlloc_object_methods [] = { + { "name", ObjWithAlloc_name, METH_NOARGS, + "name () -> String.\n\ +Return the name of the class as a string value." }, + { "whoami", ObjWithAlloc_whoami, METH_NOARGS, + "whoami () -> String.\n\ +Return the object's greeting as a string value." }, + { "speak", ObjWithAlloc_speak, METH_NOARGS, + "speak () -> String.\n\ +Return the object's speech as a string value." }, + {nullptr} /* Sentinel */ +}; + +PyType_Slot ObjWithAlloc_object_type_slot[] = { + {Py_tp_doc, const_cast("ObjWithAlloc object")}, + gdbpy_heap_type_methods (ObjWithAlloc_traverse, ObjWithAlloc_clear), + {Py_tp_alloc, (void*) ObjWithAlloc_alloc}, + {Py_tp_methods, ObjWithAlloc_object_methods}, + {0, nullptr}, /* Sentinel */ +}; + +PyType_Spec ObjWithAlloc_object_type_spec = { + "MyModule.ObjWithAlloc", /* tp_name */ + sizeof (ObjWithAlloc), /* tp_basicsize */ + 0, /* tp_itemsize */ + gdbpy_heap_type::tp_flags_default () + | Py_TPFLAGS_BASETYPE, /* tp_flags */ + ObjWithAlloc_object_type_slot, +}; + +} // namespace anonymous + +gdbpy_object_type_ref +ObjWithAlloc::record_class + (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class ATTRIBUTE_UNUSED) +{ + gdbpy_object_type_ref obj_type + = gdbpy_type_initialize (module, + &ObjWithAlloc_object_type_spec); + + ObjWithAlloc_object_type = obj_type; + return obj_type; +} diff --git a/src/gdb-others.cpp b/src/gdb-others.cpp new file mode 100644 index 0000000..10e51d1 --- /dev/null +++ b/src/gdb-others.cpp @@ -0,0 +1,31 @@ +#include "gdb-others.hpp" + +#define TOOLNAME "GDB" + +#include +#include +#include + +extern "C" { + +[[noreturn]] static void +internal_verror (const char *file, int line, const char *fmt, va_list args) +{ + fprintf (stderr, "\ +%s:%d: A problem internal to " TOOLNAME " has been detected.\n", file, line); + vfprintf (stderr, fmt, args); + fprintf (stderr, "\n"); + abort (); +} + +void +internal_error_loc (const char *file, int line, const char *fmt, ...) +{ + va_list ap; + + va_start (ap, fmt); + internal_verror (file, line, fmt, ap); + va_end (ap); +} + +} diff --git a/src/my_py_module.cpp b/src/my_py_module.cpp new file mode 100644 index 0000000..edb62cc --- /dev/null +++ b/src/my_py_module.cpp @@ -0,0 +1,29 @@ +#include + +#include "my_py_module.hpp" + +namespace { + +// A struct contains the definition of a module +PyModuleDef my_module = { + PyModuleDef_HEAD_INIT, + "MyModule", + "This is MyModule's docstring", + -1, // Optional size of the module state memory + NULL, // Optional module methods + NULL, // Optional slot definitions + NULL, // Optional traversal function + NULL, // Optional clear function + NULL // Optional module deallocation function +}; + +} // namespace anonymous + +PyMODINIT_FUNC +PyInit_MyModule(void) { + gdbpy_borrowed_ref<> module = PyModule_Create(&my_module); + auto A_object_type = A::record_class (module); + auto B_object_type = B::record_class (module, A_object_type); + auto ObjWithAlloc_object_type = ObjWithAlloc::record_class (module); + return module; +} diff --git a/src/my_py_module.hpp b/src/my_py_module.hpp new file mode 100644 index 0000000..6fc173f --- /dev/null +++ b/src/my_py_module.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include "python-internal.hpp" + +#include +#include + +struct A: public PyObject +{ + /* Dictionary holding user-added attributes. + This is the __dict__ attribute of the object. */ + GDBPYOBJECT_DICT_ATTR (A); + + PyObject *m_a; + + static gdbpy_object_type_ref + record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class = nullptr); +}; + +static_assert (gdb::is_python_allocatable_v); + +struct B: public A +{ + PyObject *m_b; + + static gdbpy_object_type_ref + record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class = nullptr); +}; + +static_assert (gdb::is_python_allocatable_v); + +struct ObjWithAlloc: public PyObject +{ + const std::string *msg; + PyObject *m_name; + PyObject *self_cycle; + + static gdbpy_object_type_ref + record_class (gdbpy_borrowed_ref<> module, + gdbpy_object_type_bref base_class = nullptr); +}; + +static_assert (gdb::is_python_allocatable_v); diff --git a/src/py-obj-type.cpp b/src/py-obj-type.cpp new file mode 100644 index 0000000..6add108 --- /dev/null +++ b/src/py-obj-type.cpp @@ -0,0 +1,80 @@ +/* Helpers related to Python object type + + Copyright (C) 2026 Free Software Foundation, Inc. + + This file is part of GDB. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . */ + +#include "python-internal.hpp" +#include "py-obj-type.hpp" + +/* Return the type's fully qualified name from a PyTypeObject. */ +std::string +gdb_py_tp_name (PyTypeObject *py_type) noexcept +{ + auto pyobj_to_str = [](PyObject *name) -> std::string + { + const char *s = PyUnicode_AsUTF8AndSize (name, nullptr); + return (s == nullptr) ? "" : s; + }; + +#if PY_VERSION_HEX >= 0x030d0000 + /* Note: PyType_GetFullyQualifiedName() was added in version 3.13, and is + part of the stable ABI since version 3.13. */ + gdbpy_ref<> fully_qualified_name (PyType_GetFullyQualifiedName (py_type)); + if (fully_qualified_name == nullptr) + return {}; + + return pyobj_to_str (fully_qualified_name.get ()); + +#else /* PY_VERSION_HEX < 0x030d0000 && ! defined (Py_LIMITED_API) */ + /* For non-heap types, the fully qualified name corresponds to tp_name. */ + if (! (PyType_GetFlags (py_type) & Py_TPFLAGS_HEAPTYPE)) + return py_type->tp_name; + + /* In the absence of PyType_GetFullyQualifiedName(), we fallback using + __qualname__ instead. However, the result may differ slightly in some + cases, e.g. the module name may be missing. */ + +# if PY_VERSION_HEX >= 0x030b0000 + /* Note: PyType_GetQualName() was added in version 3.11. */ + gdbpy_ref<> qualname (PyType_GetQualName (py_type)); + if (qualname == nullptr) + return {}; + + return pyobj_to_str (qualname.get ()); + +# else + /* In the absence of PyType_GetQualName(), fallback on using PyHeapTypeObject + which is not part of the public API. + Tested on 3.10 which is the oldest supported version at the time of this + writing, i.e. February 2026. Hopefully, this workaround should go away + when the minimum supported Python version is increased above 3.10. */ + PyHeapTypeObject *ht = (PyHeapTypeObject *) py_type; + if (ht->ht_qualname == nullptr) + return {}; + + return pyobj_to_str (ht->ht_qualname); +# endif +#endif +} + +/* Return the type's fully qualified name from a PyObject. */ +std::string +gdbpy_py_obj_tp_name (PyObject *self) noexcept +{ + /* Note: Py_TYPE () is part of the stable ABI since version 3.14. */ + return gdb_py_tp_name (Py_TYPE (self)); +} diff --git a/src/python-internal.cpp b/src/python-internal.cpp new file mode 100644 index 0000000..6b1a262 --- /dev/null +++ b/src/python-internal.cpp @@ -0,0 +1,87 @@ +#include "python-internal.hpp" +#include + +namespace { + +/* Like PyModule_AddObject, but does not steal a reference to + OBJECT. */ + +bool +gdb_pymodule_addobject (PyObject *mod, const char *name, PyObject *object) +{ + Py_INCREF (object); + int result = PyModule_AddObject (mod, name, object); + if (result < 0) + Py_DECREF (object); + return result == 0; +} + +} // namespace anonymous + +/* A wrapper for all the initialization logic of the Python type. + - PyType_FromSpec or PyType_FromSpecWithBases initializes the opaque type + PyTypeObject (which is itself a PyObject) from the provided PyType_Spec. + - gdb_pymodule_addobject registers the type in the appropriate module. + If MOD is supplied, then the type is added to that module. If MOD + is not supplied, the type name (PyType_Spec.name field) must be of + the form "gdb.Mumble", and the type will be added to the gdb module. + Returns true on success, false on error. */ + +gdbpy_object_type_ref +gdbpy_type_initialize (gdbpy_borrowed_ref<> &mod, + PyType_Spec *spec, + PyObject *bases, + bool disallow_instantiation) +{ + gdbpy_ref<> res (bases == nullptr + ? PyType_FromSpec (spec) + : PyType_FromSpecWithBases (spec, bases)); + if (res == nullptr) + return nullptr; + + gdbpy_object_type_ref object_type (std::move (res)); + + if (object_type.same_as (nullptr)) + { + PyErr_Print (); + return nullptr; + } + +#if PY_VERSION_HEX < 0x030a0000 + /* When using the Python unlimited API, disallowing the creation of instances + of the type in Python is done by setting tp_free to NULL. + When using the Python limited API, PyType_FromSpec and its siblings + automatically set tp_new to PyType_GenericNew(), which has for consequence + of allowing the creation of instances of the type in Python. + To disallow this behavior with the limited API, a new flag was introduced + in Python 3.9: Py_TPFLAGS_DISALLOW_INSTANTIATION. + For Python versions older than 3.9, we need to set explicitly tp_free to + NULL once the type object was created. This workaround is only available + when the limited API is disabled. */ +#if defined (Py_LIMITED_API) + #error "The version of Python you are using does not expose " \ + "Py_TPFLAGS_DISALLOW_INSTANTIATION as a part of the limited C API." +#else + if (disallow_instantiation) + object_type.pytype ()->tp_new = nullptr; + + /* PyType_Ready() is already called at the end of PyType_FromMetaclass() + (called by both PyType_FromSpec and PyType_FromSpecWithBases), so no + need to call it again unless the Python version does not have the flag + Py_TPFLAGS_DISALLOW_INSTANTIATION. In this case, a second call after + setting tp_new to NULL will disallow the instantiation for the type as + it used to be the case with non-heap types. */ + if (PyType_Ready (object_type) < 0) + return nullptr; +#endif +#else + (void) disallow_instantiation; +#endif + + const char *dot = strrchr (spec->name, '.'); + gdb_assert (dot != nullptr); + if (gdb_pymodule_addobject (mod.get (), dot + 1, object_type.get ()) < 0) + return nullptr; + + return object_type; +} -- 2.54.0